🐶 Sync 2026-10-03 11:32:07

This commit is contained in:
github-actions[bot] committed 2026-10-03 11:32:07 +08:00
1 parent a5bb91e6ed
commit 66e3d835c4
72 files changed
+5107 -1799

No files matched your search

+2 -2
View File
@@ -5,12 +5,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=antileech
PKG_RELEASE:=4
PKG_RELEASE:=5
PKG_SOURCE_PROTO:=git
PKG_SOURCE_DATE:=2021-12-05
PKG_SOURCE_URL:=https://github.com/persmule/amule-dlp.antiLeech.git
PKG_SOURCE_VERSION:=bc3ae6a86c958dab1bde9c2cea51b523530aec4b
PKG_SOURCE_VERSION:=27f00cc98e9548777ce717f55fc760b73230b563
PKG_MIRROR_HASH:=skip
PKG_FIXUP:=autoreconf
+1 -1
View File
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter
LUCI_PKGARCH:=all
PKG_VERSION:=0.1.50
PKG_RELEASE:=56
PKG_RELEASE:=57
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH
@@ -80,6 +80,7 @@ function renderFilterChips(host, state, callbacks) {
const parsed = log.parseFilterValue(val);
if (!parsed.value) continue;
const negated = parsed.negate;
const invertLabel = String(negated ? _('Include instead') : _('Exclude instead'));
const kids = [];
const lead = chipLeadingSym(negated);
if (lead) kids.push(lead);
@@ -90,7 +91,7 @@ function renderFilterChips(host, state, callbacks) {
'span',
{
'class': 'fwlive-chip-invert-wrap',
'data-tip': negated ? _('Include instead') : _('Exclude instead')
'data-tip': invertLabel
},
[
E(
@@ -98,6 +99,7 @@ function renderFilterChips(host, state, callbacks) {
{
'type': 'button',
'class': 'fwlive-chip-invert',
'aria-label': invertLabel,
'click': function (ev) {
callbacks.onInvert(spec.key, ev);
}
@@ -11,5 +11,5 @@
* GENERATED — do not edit. Edit fwlive.css and run: node scripts/embed-fwlive-css.js
*/
return baseclass.extend({
styleText: "\n/* SPDX-License-Identifier: Apache-2.0 */\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-dLine truncated
styleText: "\n/* SPDX-License-Identifier: Apache-2.0 */\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-dLine truncated
});
@@ -790,6 +790,32 @@
.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-expand {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.5em;
min-width: 24px;
height: 24px;
padding: 0;
margin-left: 3px;
border: 1px solid var(--border-color-medium);
border-radius: 3px;
background: transparent;
color: var(--text-color-medium);
font: inherit;
line-height: 1;
vertical-align: middle;
cursor: pointer;
}
.fwlive-row-expand:hover {
color: var(--primary-color-high);
border-color: var(--primary-color-medium, var(--primary-color-high));
}
.fwlive-row-expand:focus-visible {
outline: 2px solid var(--primary-color-high);
outline-offset: 2px;
}
.fwlive-row-expanded td { border-bottom-color: transparent; }
.fwlive-msg-expand td {
background: var(--background-color-low);
@@ -64,6 +64,31 @@ const CLASSIFY_SPEC = {
/* Validate the trusted module configuration once, before constructing regexes. */
validateClassifySpec(CLASSIFY_SPEC);
/* Compile only the finite KV vocabulary from the validated static rules. */
const KV_HAS_PATTERNS = Object.create(null);
function compileKvHasPatterns(node) {
const keys = Object.keys(node);
const operator = keys[0];
if (operator === 'and' || operator === 'or') {
for (let i = 0; i < node[operator].length; i++) compileKvHasPatterns(node[operator][i]);
return;
}
if (operator !== 'kv' && operator !== 'kvAny') return;
const names = node[operator];
for (let i = 0; i < names.length; i++) {
const name = names[i];
if (!Object.prototype.hasOwnProperty.call(KV_HAS_PATTERNS, name)) {
KV_HAS_PATTERNS[name] = new RegExp('(^|[^A-Za-z0-9_])' + name + '=');
}
}
}
for (let i = 0; i < CLASSIFY_SPEC.rules.length; i++) {
compileKvHasPatterns(CLASSIFY_SPEC.rules[i]);
}
function validateClassifySpec(spec) {
const validateNode = function (node) {
if (!node || typeof node !== 'object' || Array.isArray(node))
@@ -165,6 +190,11 @@ return baseclass.extend({
NETFILTER_KV_GLUE: NETFILTER_KV_GLUE,
kvHas: function (msg, key) {
if (typeof key === 'string' && Object.prototype.hasOwnProperty.call(KV_HAS_PATTERNS, key)) {
return KV_HAS_PATTERNS[key].test(msg);
}
/* Preserve direct, non-spec calls without retaining arbitrary keys. */
return new RegExp('(^|[^A-Za-z0-9_])' + key + '=').test(msg);
},
@@ -55,6 +55,8 @@ function columnLabel(col) {
return labels[col] || col;
}
const EXPANSION_PANEL_ID = 'fwlive-expanded-message';
function dirLabel(dir) {
const labels = {
'in': _('Inbound'),
@@ -144,6 +146,28 @@ function flowCell(row, state, callbacks) {
return E('span', { 'class': 'fwlive-flow' }, parts);
}
function rowExpandButton(row, state, callbacks) {
const expanded = state.expandedRowId === row.id;
const action = expanded ? _('Hide full message') : _('Show full message');
const button = E(
'button',
{
'type': 'button',
'class': 'fwlive-row-expand',
'aria-label': String(action),
'aria-expanded': expanded ? 'true' : 'false',
'aria-controls': expanded ? EXPANSION_PANEL_ID : null,
'click': function (ev) {
if (ev && ev.stopPropagation) ev.stopPropagation();
callbacks.onRowClick(row.id, ev);
}
},
[expanded ? '▾' : '▸']
);
button._fwliveRowId = String(row.id);
return button;
}
function buildColumnCell(col, row, state, callbacks) {
const onFilterClick = callbacks.onFilterClick;
const msgDisplay = log.formatMessageDisplay(row.message, state.messageLayout);
@@ -163,8 +187,12 @@ function buildColumnCell(col, row, state, callbacks) {
: log.formatTimestampLocal(row.timestamp)
]);
}
case 'action':
return E('td', { 'class': log.actionRowClass(row.action) }, [actionCell]);
case 'action': {
const actionChildren = [actionCell];
if (state.viewMode === 'simple')
actionChildren.push(' ', rowExpandButton(row, state, callbacks));
return E('td', { 'class': log.actionRowClass(row.action) }, actionChildren);
}
case 'rule':
return E('td', { 'class': columnCellClass(col) }, [
links.ruleAdminLink(row.rule_hint, row.rule_label, onFilterClick)
@@ -338,7 +366,7 @@ function buildExpansionRow(row, state, columns) {
const expansion = 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' }, [
E('pre', { 'id': EXPANSION_PANEL_ID, 'class': 'fwlive-msg-expand-body' }, [
/* oneline is uncapped; wrap ellipsizes at 240 */
log.formatMessageDisplay(row.message, 'oneline') || '—'
])
@@ -100,6 +100,36 @@ function optionNodes(pairs) {
return opts;
}
function expansionButtonRowId(button) {
if (
!button ||
!button.classList ||
!button.classList.contains('fwlive-row-expand') ||
button._fwliveRowId == null
)
return null;
return String(button._fwliveRowId);
}
function restoreExpansionFocus(body, rowId, previousFocus) {
if (!body || typeof body.querySelectorAll !== 'function' || rowId == null) return false;
const buttons = body.querySelectorAll('button.fwlive-row-expand');
const activeElement = typeof document !== 'undefined' ? document.activeElement : null;
for (let i = 0; i < buttons.length; i++) {
const button = buttons[i];
if (String(button._fwliveRowId) !== String(rowId)) continue;
if (
(button !== previousFocus || activeElement !== button) &&
typeof button.focus === 'function'
)
button.focus();
return true;
}
return false;
}
function isIpv4Address(addr) {
if (addr.indexOf('.') === -1) return false;
@@ -687,8 +717,9 @@ return view.extend({
if (ev && ev.target && ev.target.closest && ev.target.closest('a.fwlive-filter-link'))
return;
const focusRowId = expansionButtonRowId(ev && ev.currentTarget);
this.expandedRowId = this.expandedRowId === rowId ? null : rowId;
this.renderRows(true);
this.renderRows(true, focusRowId);
},
renderThead() {
@@ -2158,12 +2189,15 @@ return view.extend({
this.renderRows(true);
},
renderRows(force) {
renderRows(force, focusRowId) {
const el = document.getElementById('fwlive-table');
if (!el || typeof el.querySelector !== 'function') return;
const body = el.querySelector('tbody');
const scroll = document.getElementById('fwlive-scroll');
const activeElement = typeof document !== 'undefined' ? document.activeElement : null;
const activeRowId = expansionButtonRowId(activeElement);
const restoreRowId = focusRowId != null ? String(focusRowId) : activeRowId;
const filtered = this.filteredRowsState();
const rows = filtered.rows.slice();
@@ -2209,10 +2243,11 @@ return view.extend({
actionRowTintClass: (action) => this.actionRowTintClass(action)
}
);
const expansionFocusKept = restoreExpansionFocus(body, restoreRowId, activeElement);
if (force && !this.isTabHidden()) this.resolvePaintPending = false;
if (scroll) {
if (!this.tablePaused && this.followLive) scroll.scrollTop = 0;
if (!this.tablePaused && this.followLive && !expansionFocusKept) scroll.scrollTop = 0;
else scroll.scrollTop = prevScroll;
}
@@ -2541,7 +2576,7 @@ return view.extend({
E('li', { 'id': 'fwlive-manual-test' }, []),
E('li', {}, [
_(
'Click a row (Time or other non-link cells) to see the full log line (Simple view).'
'In Simple view, activate the message button or click a row to show or hide the full log line.'
)
]),
E('li', {}, [
+188 -177
View File
@@ -12,56 +12,56 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1697
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1728
msgid "%d matching · %d/%d stored"
msgstr "%d Treffer · %d/%d gespeichert"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1662
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1693
msgid "%dd"
msgstr "%dd"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1661
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692
msgid "%dh"
msgstr "%dh"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1660
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1691
msgid "%dm"
msgstr "%dm"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1659
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690
msgid "%ds"
msgstr "%ds"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1423
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1454
msgid "%s · requested up to %d raw lines · %d firewall messages returned"
msgstr "%s · bis zu %d Rohzeilen angefordert · %d Firewall-Meldungen zurückgegeben"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1413
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1444
msgid "%s · requested up to %d raw lines · server limited fetch to %d · %d firewall messages returned"
msgstr "%s · bis zu %d Rohzeilen angefordert · Server begrenzte Abruf auf %d · %d Firewall-Meldungen zurückgegeben"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:543
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:550
msgid "%s: %s"
msgstr "%s: %s"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1699
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1730
msgid "0 matching · %d/%d stored"
msgstr "0 Treffer · %d/%d gespeichert"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:453
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:483
msgid "Accessible (teal/orange)"
msgstr "Barrierefrei (teal/orange)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:403
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:410
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38
msgid "Action"
msgstr "Aktion"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:946
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977
msgid "Administrator access is required to disable logging."
msgstr "Zum Deaktivieren der Protokollierung sind Administratorrechte erforderlich."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:934
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:965
msgid "Administrator access is required to enable logging."
msgstr "Zum Aktivieren der Protokollierung sind Administratorrechte erforderlich."
@@ -69,7 +69,7 @@ msgstr "Zum Aktivieren der Protokollierung sind Administratorrechte erforderlich
msgid "Allow/deny rules, LAN logging, or anything else."
msgstr "Allow/Deny-Regeln, LAN-Protokollierung oder irgendetwas anderes."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1906
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1937
msgid "Also seen"
msgstr "Weitere"
@@ -81,16 +81,16 @@ msgstr "Eine andere Änderung an der Firewall ist vorgemerkt; zuerst anwenden od
msgid "Another change overwrote WAN logging after it was saved; check the current state."
msgstr "Eine andere Änderung hat die WAN-Protokollierung nach dem Speichern überschrieben; prüfen Sie den aktuellen Zustand."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2755
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2792
msgid "Any action"
msgstr "Beliebige Aktion"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1899
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1930
msgid "Any protocol"
msgstr "Beliebiges Protokoll"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1404
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1476
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1435
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507
msgid "Auto"
msgstr "Auto"
@@ -106,39 +106,35 @@ msgstr "Protokollierung kann erst aktiviert werden, wenn die Kernel-Log-Module i
msgid "Changes:"
msgstr "Änderungen:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:452
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:482
msgid "Classic (green/red)"
msgstr "Klassisch (grün/rot)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2467
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2504
msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr "Klassisch verwendet grün/rot; Barrierefrei verwendet teal/orange"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:157
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:159
msgid "Clear all"
msgstr "Alle löschen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2827
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2864
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"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2542
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)."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:159
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:183
msgid "Click a row for the full message"
msgstr "Zeile anklicken für die vollständige Meldung"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2547
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2584
msgid "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a chip) to exclude."
msgstr "Klicken Sie auf eine IP, Aktion oder ein Protokoll, um zu filtern; nutzen Sie das Protokoll-Menü (oder ≠ auf einem Chip) zum Ausschließen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1900
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1931
msgid "Common"
msgstr "Häufig"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1710
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1741
msgid "Connection lost — retrying…"
msgstr "Verbindung verloren — neuer Versuch…"
@@ -150,43 +146,43 @@ msgstr "Die Protokollierungssperre konnte nicht erworben werden."
msgid "Could not clear the WAN zone log option."
msgstr "Die WAN-Zonen-Log-Option konnte nicht gelöscht werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:757
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:788
msgid "Could not create a temporary file for rule names."
msgstr "Für die Regelnamen konnte keine temporäre Datei erstellt werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:944
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:975
msgid "Could not disable logging."
msgstr "Protokollierung konnte nicht deaktiviert werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:926
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:957
msgid "Could not enable logging."
msgstr "Protokollierung konnte nicht aktiviert werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:860
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:891
msgid "Could not load logging status."
msgstr "Protokollstatus konnte nicht geladen werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:763
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:794
msgid "Could not load rule names."
msgstr "Die Regelnamen konnten nicht geladen werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:759
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:790
msgid "Could not process firewall rule names."
msgstr "Die Namen der Firewall-Regeln konnten nicht verarbeitet werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:761
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:792
msgid "Could not read the active firewall rules."
msgstr "Die aktiven Firewall-Regeln konnten nicht gelesen werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:859
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:890
msgid "Could not refresh logging status; showing the last known state."
msgstr "Protokollstatus konnte nicht aktualisiert werden; zeige den zuletzt bekannten Zustand."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:768
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:799
msgid "Could not refresh rule names."
msgstr "Die Regelnamen konnten nicht aktualisiert werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:766
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:797
msgid "Could not refresh rule names; previously loaded names are still shown."
msgstr "Die Regelnamen konnten nicht aktualisiert werden; die zuvor geladenen Namen werden weiterhin angezeigt."
@@ -206,7 +202,7 @@ msgstr "Die Änderung der Protokollierung konnte nicht sicher nachverfolgt werde
msgid "Could not write the WAN zone log option."
msgstr "Die WAN-Zonen-Log-Option konnte nicht geschrieben werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2782
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr "Eigenes Protokoll (! zum Ausschließen). Überschreibt das Menü, wenn gesetzt."
@@ -214,28 +210,28 @@ msgstr "Eigenes Protokoll (! zum Ausschließen). Überschreibt das Menü, wenn g
msgid "DPort"
msgstr "Ziel-Port"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:407
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:414
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46
msgid "Destination"
msgstr "Ziel"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2812
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2849
msgid "Destination IP contains (! to exclude)"
msgstr "Ziel-IP enthält (! zum Ausschließen)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:409
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:416
msgid "Destination port"
msgstr "Ziel-Port"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2817
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2854
msgid "Destination port (! to exclude)"
msgstr "Ziel-Port (! zum Ausschließen)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2670
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2707
msgid "Detail"
msgstr "Detail"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2660
msgid "Details"
msgstr "Details"
@@ -247,15 +243,15 @@ msgstr "Richtung"
msgid "Disabling…"
msgstr "Wird deaktiviert…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1453
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1484
msgid "Display limited to %d rows on this device; the larger Limit still buffers rows."
msgstr "Anzeige auf diesem Gerät auf %d Zeilen begrenzt; das größere Limit puffert weiterhin Zeilen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2408
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2445
msgid "Display options"
msgstr "Anzeigeoptionen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2501
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2538
msgid "Display options set Limit, Fetch budget, Maximum raw lines, row tint, palette, and hostnames."
msgstr "Anzeigeoptionen setzen Limit, Abrufbudget, maximale Rohzeilen, Zeileneinfärbung, Palette und Hostnamen."
@@ -275,7 +271,7 @@ msgstr "Drop/Reject-Protokollierung der WAN-Zone aktivieren (wie unter Netzwerk
msgid "Enable logging"
msgstr "Protokollierung aktivieren"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2496
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2533
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 "„Protokollierung aktivieren“ schaltet nur die Drop/Reject-Protokollierung der WAN-Zone ein (wie unter Netzwerk → Firewall). Es fügt keine Regeln hinzu und protokolliert keinen normalen LAN-Verkehr."
@@ -283,19 +279,19 @@ msgstr "„Protokollierung aktivieren“ schaltet nur die Drop/Reject-Protokolli
msgid "Enabling…"
msgstr "Wird aktiviert…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1913
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1944
msgid "Exclude"
msgstr "Ausschließen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:83
msgid "Exclude instead"
msgstr "Stattdessen ausschließen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2422
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2459
msgid "Fetch budget"
msgstr "Abrufbudget"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2511
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548
msgid "Fetch budget controls raw log lines per poll. Auto derives from Limit; Manual selects a bounded maximum."
msgstr "Das Abrufbudget steuert die Rohzeilen pro Abfrage. Auto leitet es aus dem Limit ab; Manuell wählt ein begrenztes Maximum."
@@ -309,12 +305,12 @@ msgstr "Filtern nach %s"
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."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2585
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2622
#: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View"
msgstr "Firewall-Live-Ansicht"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1677
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1708
msgid "Firewall log has no current entries; showing rows last received %s ago."
msgstr "Im Firewall-Protokoll sind derzeit keine Einträge; angezeigt werden Zeilen, die zuletzt vor %s empfangen wurden."
@@ -326,11 +322,11 @@ msgstr "Flags"
msgid "Flow"
msgstr "Fluss"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2521
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2558
msgid "For a responsive table on a weak device, keep Limit at 250 rows or below. The weak-device cap affects rendered rows; the buffer can still retain more."
msgstr "Für eine responsive Tabelle auf einem schwachen Gerät sollte Limit auf höchstens 250 Zeilen stehen. Die Schwachgerätebegrenzung betrifft die angezeigten Zeilen; der Puffer kann weiterhin mehr Zeilen behalten."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:64
msgid "Forwarded"
msgstr "Weitergeleitet"
@@ -338,19 +334,23 @@ msgstr "Weitergeleitet"
msgid "Grant access to firewall live log view"
msgstr "Zugriff auf die Live-Ansicht der Firewall-Protokolle gewähren"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2487
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2524
msgid "Help"
msgstr "Hilfe"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:151
msgid "Hide full message"
msgstr "Vollständige Nachricht ausblenden"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582
msgid "Hide rows"
msgstr "Zeilen ausblenden"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1449
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1480
msgid "Hostname lookup paused while the router is under load."
msgstr "Hostnamenauflösung pausiert, solange der Router ausgelastet ist."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1904
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1935
msgid "ICMPv6"
msgstr "ICMPv6"
@@ -358,7 +358,7 @@ msgstr "ICMPv6"
msgid "IN"
msgstr "EINGANG"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2558
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2595
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)."
@@ -366,11 +366,15 @@ msgstr "Wenn die Zeileneinfärbung fehlt, verwendet das aktive LuCI-Theme mögli
msgid "If the WAN is quiet, wait for probes or use the optional ping check in Help / the enabling-logs guide."
msgstr "Wenn das WAN ruhig ist, warten Sie auf Probes oder nutzen Sie den optionalen Ping-Test in der Hilfe / der Anleitung zum Aktivieren der Protokollierung."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:60
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2579
msgid "In Simple view, activate the message button or click a row to show or hide the full log line."
msgstr "In der einfachen Ansicht können Sie die Nachrichtenschaltfläche aktivieren oder auf eine Zeile klicken, um die vollständige Protokollzeile ein- oder auszublenden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62
msgid "Inbound"
msgstr "Eingehend"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:83
msgid "Include instead"
msgstr "Stattdessen einschließen"
@@ -378,16 +382,16 @@ msgstr "Stattdessen einschließen"
msgid "Install kmod-nf-log and kmod-nf-log6 with the command for your OpenWrt release, then reload the firewall."
msgstr "Installieren Sie kmod-nf-log und kmod-nf-log6 mit dem Befehl für Ihre OpenWrt-Version und laden Sie dann die Firewall neu."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1708
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1739
msgid "Installation is incomplete. Reinstall luci-app-fwlive."
msgstr "Installation unvollständig. Installieren Sie luci-app-fwlive neu."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:404
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:411
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40
msgid "Interface"
msgstr "Schnittstelle"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2797
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2834
msgid "Interface (prefix ! to exclude)"
msgstr "Schnittstelle (! voranstellen zum Ausschließen)"
@@ -404,11 +408,11 @@ msgstr "Kernel-Logmodule fehlen"
msgid "Len"
msgstr "Länge"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2411
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2448
msgid "Limit"
msgstr "Limit"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:814
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:845
msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr "Die Live-Ansicht kann unvollständig sein — eine veraltete iptables-Tabelle ist registriert"
@@ -416,16 +420,16 @@ msgstr "Die Live-Ansicht kann unvollständig sein — eine veraltete iptables-Ta
msgid "Logging is off on this router"
msgstr "Protokollierung ist auf diesem Router deaktiviert"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1404
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1477
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1435
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1508
msgid "Manual"
msgstr "Manuell"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2440
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2477
msgid "Manual still uses server protection and poll cadence"
msgstr "Manuell nutzt weiterhin Serverschutz und Abfrageintervall"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2516
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2553
msgid "Manual still uses server protection and poll cadence; it changes the fetch budget, not the polling interval."
msgstr "Manuell nutzt weiterhin Serverschutz und Abfrageintervall; es ändert das Abrufbudget, nicht das Abfrageintervall."
@@ -433,18 +437,18 @@ msgstr "Manuell nutzt weiterhin Serverschutz und Abfrageintervall; es ändert da
msgid "Manual test (System → Terminal):"
msgstr "Manueller Test (System → Terminal):"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2434
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2471
msgid "Maximum raw lines"
msgstr "Maximale Rohzeilen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:52
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:340
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2682
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2689
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:368
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2719
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2726
msgid "Message"
msgstr "Nachricht"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2792
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2829
msgid "More filters"
msgstr "Weitere Filter"
@@ -476,7 +480,7 @@ msgstr "Nichts ändert sich, bis Sie auf „Protokollierung aktivieren“ klicke
msgid "OUT"
msgstr "AUSGANG"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2712
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2749
msgid "One line"
msgstr "Einzeilig"
@@ -496,57 +500,57 @@ msgstr "OpenWrt 25.12 und neuer (apk):"
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 schreibt Firewall-Ereignisse erst ins Log, wenn Sie die Protokollierung aktivieren. Die Live-Ansicht zeigt nur, was die Firewall bereits protokolliert — sie fügt keine Allow/Deny-Regeln hinzu."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:61
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:63
msgid "Outbound"
msgstr "Ausgehend"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2461
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2498
msgid "Palette"
msgstr "Palette"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2636
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2673
msgid "Pause"
msgstr "Pause"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1770
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801
msgid "Paused"
msgstr "Pausiert"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1436
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1467
msgid "Polling slowed: the router is responding slowly, so polls run less often."
msgstr "Abfragen verlangsamt: Der Router antwortet langsam, daher wird seltener abgefragt."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:405
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:412
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44
msgid "Proto"
msgstr "Protokoll"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2773
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2810
msgid "Protocol — common values"
msgstr "Protokoll — häufige Werte"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2752
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2789
msgid "Quick search"
msgstr "Schnellsuche"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:118
msgid "Remove filter"
msgstr "Filter entfernen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802
msgid "Resume"
msgstr "Fortsetzen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2451
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2488
msgid "Row tint"
msgstr "Zeileneinfärbung"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2589
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."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2608
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2645
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."
@@ -554,19 +558,19 @@ msgstr "Die Zeileneinfärbung verwendete einen lokalen Farbfallback, da das akti
msgid "Rule"
msgstr "Regel"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2660
msgid "Rule name details"
msgstr "Details zu Regelnamen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:785
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:816
msgid "Rule-name lookup does not change your firewall rules."
msgstr "Das Laden der Regelnamen verändert Ihre Firewall-Regeln nicht."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:770
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:801
msgid "Rule-name lookup failed."
msgstr "Das Laden der Regelnamen ist fehlgeschlagen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:777
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
msgid "Rule-name lookup reached a safety limit."
msgstr "Beim Laden der Regelnamen wurde eine Schutzgrenze erreicht."
@@ -574,93 +578,97 @@ msgstr "Beim Laden der Regelnamen wurde eine Schutzgrenze erreicht."
msgid "SPort"
msgstr "Quell-Port"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:402
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:409
msgid "Search"
msgstr "Suche"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1433
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1464
msgid "Server adaptive protection is disabled."
msgstr "Adaptiver Serverschutz ist deaktiviert."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1441
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1472
msgid "Server limited fetch: the router is under load; at most %d log lines per poll."
msgstr "Abruf vom Server begrenzt: Der Router ist ausgelastet; höchstens %d Protokollzeilen pro Abfrage."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1446
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1477
msgid "Server limited fetch: this poll returned fewer lines to protect the router."
msgstr "Abruf vom Server begrenzt: Diese Abfrage lieferte weniger Zeilen, um den Router zu schonen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1431
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1462
msgid "Server protection state is unknown."
msgstr "Serverschutz-Status unbekannt."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2478
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:151
msgid "Show full message"
msgstr "Vollständige Nachricht anzeigen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2515
msgid "Show hostnames"
msgstr "Hostnamen anzeigen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2449
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2486
msgid "Show pass/deny row background colors"
msgstr "Pass/Deny-Zeilenhintergrundfarben anzeigen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2772
msgid "Show rows"
msgstr "Zeilen anzeigen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1511
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1542
msgid "Showing a compact summary of the top of the shown sample."
msgstr "Eine kompakte Zusammenfassung des Anfangs der angezeigten Stichprobe."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2659
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2696
msgid "Simple"
msgstr "Einfach"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:788
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:819
msgid "Some entries may show names derived from their log prefixes. Searching by a friendly rule name may miss those entries."
msgstr "Bei einigen Einträgen werden möglicherweise Namen aus den Protokollpräfixen angezeigt. Bei der Suche nach einem lesbaren Regelnamen werden diese Einträge möglicherweise nicht gefunden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:839
msgid "Some rule names may be missing"
msgstr "Einige Regelnamen fehlen möglicherweise"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:406
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:413
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45
msgid "Source"
msgstr "Quelle"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2802
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2839
msgid "Source IP contains (! to exclude)"
msgstr "Quell-IP enthält (! zum Ausschließen)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:408
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:415
msgid "Source port"
msgstr "Quell-Port"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2807
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2844
msgid "Source port (! to exclude)"
msgstr "Quell-Port (! zum Ausschließen)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1513
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1544
msgid "Summary data is unavailable; show rows to inspect the sample."
msgstr "Zusammenfassungsdaten nicht verfügbar; Zeilen anzeigen, um die Stichprobe zu prüfen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2726
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2763
msgid "Summary mode"
msgstr "Zusammenfassungsmodus"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2506
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2543
msgid "Summary mode: when the router responds slowly, a compact summary replaces the table. Use Show rows to see the table; it returns automatically after three fast polls."
msgstr "Zusammenfassungsmodus: Wenn der Router langsam antwortet, ersetzt eine kompakte Zusammenfassung die Tabelle. Mit „Zeilen anzeigen“ sehen Sie die Tabelle; sie kehrt nach drei schnellen Abfragen automatisch zurück."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1546
msgid "Summary was bounded before all categories could be included."
msgstr "Die Zusammenfassung wurde begrenzt, bevor alle Kategorien aufgenommen werden konnten."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2526
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563
msgid "Switching tabs pauses polling; returning performs one catch-up poll. Hostnames are off by default because lookups add work."
msgstr "Beim Wechseln des Tabs wird das Polling pausiert; bei der Rückkehr erfolgt eine einmalige Aufholabfrage. Hostnamen sind standardmäßig deaktiviert, da Auflösungen zusätzliche Arbeit verursachen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1606
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1637
msgid "Table refresh throttled: the event rate is high, so the table repaints less often to protect the browser. The buffer still updates; refresh resumes automatically."
msgstr "Tabellenaktualisierung gedrosselt: Die Ereignisrate ist hoch, daher wird die Tabelle seltener neu gezeichnet, um den Browser zu schonen. Der Puffer wird weiter aktualisiert; die Aktualisierung wird automatisch fortgesetzt."
@@ -668,23 +676,23 @@ msgstr "Tabellenaktualisierung gedrosselt: Die Ereignisrate ist hoch, daher wird
msgid "The firewall did not reload; saved and live logging may differ."
msgstr "Die Firewall wurde nicht neu geladen; gespeicherte und aktive Protokollierung können voneinander abweichen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2536
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2573
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."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1709
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1740
msgid "The router could not read the firewall log — retrying…"
msgstr "Der Router konnte das Firewall-Protokoll nicht lesen — neuer Versuch…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2531
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2568
msgid "The router log ring may evict older events before fwlive reads them. fwlive cannot recover evicted entries or change forwarding behavior."
msgstr "Der Protokollring des Routers kann ältere Ereignisse verdrängen, bevor fwlive sie liest. fwlive kann verdrängte Einträge nicht wiederherstellen und ändert das Weiterleitungsverhalten nicht."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2491
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2528
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."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2611
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648
msgid "Theme tint fallback"
msgstr "Theme-Farb-Fallback"
@@ -700,15 +708,15 @@ msgstr "Dieser Router hat eine Protokollierungsblockade gemeldet, die Live View
msgid "Time"
msgstr "Zeit"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1520
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
msgid "Top drops"
msgstr "Häufigste Drops"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1523
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1554
msgid "Top rules"
msgstr "Häufigste Regeln"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1548
msgid "Top talkers"
msgstr "Häufigste Absender"
@@ -728,16 +736,16 @@ msgstr "Aktiviert die Protokollierung für die WAN-Firewallzone und lädt die Fi
msgid "Undo:"
msgstr "Rückgängig:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2555
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2592
msgid "Use Detail for all columns (flags, length, raw message)."
msgstr "Verwenden Sie „Detail“ für alle Spalten (Flags, Länge, rohe Nachricht)."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2641
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2647
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2678
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684
msgid "View"
msgstr "Ansicht"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:976
msgid "WAN drop/reject logging is off."
msgstr "WAN-Drop/Reject-Protokollierung ist aus."
@@ -745,11 +753,11 @@ msgstr "WAN-Drop/Reject-Protokollierung ist aus."
msgid "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up here. Normal LAN browsing will not."
msgstr "WAN-Drop/Reject-Protokollierung ist an. Blockierter eingehender WAN-Verkehr erscheint hier. Normales LAN-Surfen nicht."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:930
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:961
msgid "WAN drop/reject logging is on. Blocked inbound traffic should appear here as it happens — not normal LAN browsing."
msgstr "WAN-Drop/Reject-Protokollierung ist an. Blockierter eingehender Verkehr sollte hier in Echtzeit erscheinen — nicht normales LAN-Surfen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:932
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:963
msgid "WAN logging is already enabled."
msgstr "WAN-Protokollierung ist bereits aktiviert."
@@ -778,25 +786,25 @@ msgstr "WAN-Protokollierung nicht verfügbar: keine WAN-Zone"
msgid "Waiting for firewall events"
msgstr "Warte auf Firewall-Ereignisse"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1770
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2636
msgid "Watching"
msgstr "Beobachtet"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2701
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2738
msgid "Wrap"
msgstr "Umbrechen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:72
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2757
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794
msgid "block"
msgstr "blockiert"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1255
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1286
msgid "buffer filling"
msgstr "Puffer füllt sich"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1259
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1290
msgid "buffer full"
msgstr "Puffer voll"
@@ -808,7 +816,7 @@ msgstr "enthält"
msgid "default 10/minute"
msgstr "Standard 10/Minute"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1264
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1295
msgid "display limited to %d rows"
msgstr "Anzeige auf %d Zeilen begrenzt"
@@ -816,12 +824,12 @@ msgstr "Anzeige auf %d Zeilen begrenzt"
msgid "does not contain"
msgstr "enthält nicht"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2758
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:75
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2795
msgid "drop"
msgstr "verworfen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1268
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1299
msgid "hostname lookup paused"
msgstr "Hostnamenauflösung pausiert"
@@ -829,16 +837,16 @@ msgstr "Hostnamenauflösung pausiert"
msgid "is"
msgstr "ist"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1254
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1285
msgid "loading buffer"
msgstr "Lade Puffer"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1695
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1726
msgid "loading…"
msgstr "lädt…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1493
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1524
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1533
msgid "none"
msgstr "keine"
@@ -846,101 +854,104 @@ msgstr "keine"
msgid "not"
msgstr "nicht"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1921
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1952
msgid "not AH"
msgstr "nicht AH"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1920
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1951
msgid "not ESP"
msgstr "nicht ESP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1919
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1950
msgid "not GRE"
msgstr "nicht GRE"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1916
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1947
msgid "not ICMP"
msgstr "nicht ICMP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1917
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1948
msgid "not ICMPv6"
msgstr "nicht ICMPv6"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1918
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1949
msgid "not IGMP"
msgstr "nicht IGMP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1922
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1953
msgid "not SCTP"
msgstr "nicht SCTP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1914
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1945
msgid "not TCP"
msgstr "nicht TCP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1915
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1946
msgid "not UDP"
msgstr "nicht UDP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2763
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2800
msgid "not block"
msgstr "nicht blockieren"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2762
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2799
msgid "not drop"
msgstr "nicht verwerfen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2761
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2798
msgid "not pass"
msgstr "nicht erlauben"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2764
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2801
msgid "not reject"
msgstr "nicht zurückweisen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2765
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2802
msgid "not unknown"
msgstr "nicht unbekannt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2780
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2817
msgid "or type…"
msgstr "oder tippen…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2756
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2793
msgid "pass"
msgstr "erlaubt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1266
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1297
msgid "polling slowed"
msgstr "Abfragen verlangsamt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2759
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:76
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2796
msgid "reject"
msgstr "abgelehnt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1270
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1301
msgid "scroll frozen — scroll to top to follow live"
msgstr "Scrollen eingefroren — nach oben scrollen für Live-Ansicht"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1267
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1298
msgid "server limited fetch"
msgstr "Abruf vom Server begrenzt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1261
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1292
msgid "table refresh throttled"
msgstr "Tabellenaktualisierung gedrosselt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:63
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:65
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2797
msgid "unknown"
msgstr "unbekannt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:750
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:781
msgid "using fw4"
msgstr "mit fw4"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:176
msgid "· %s"
msgstr "· %s"
#~ 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)."
+188 -177
View File
@@ -12,56 +12,56 @@ msgstr ""
"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"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1697
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1728
msgid "%d matching · %d/%d stored"
msgstr "%d совпад. · сохранено %d/%d"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1662
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1693
msgid "%dd"
msgstr "%dд"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1661
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692
msgid "%dh"
msgstr "%dч"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1660
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1691
msgid "%dm"
msgstr "%dм"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1659
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690
msgid "%ds"
msgstr "%dс"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1423
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1454
msgid "%s · requested up to %d raw lines · %d firewall messages returned"
msgstr "%s · запрошено до %d сырых строк · возвращено %d сообщений firewall"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1413
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1444
msgid "%s · requested up to %d raw lines · server limited fetch to %d · %d firewall messages returned"
msgstr "%s · запрошено до %d сырых строк · сервер ограничил выборку до %d · возвращено %d сообщений firewall"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:543
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:550
msgid "%s: %s"
msgstr "%s: %s"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1699
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1730
msgid "0 matching · %d/%d stored"
msgstr "0 совпад. · сохранено %d/%d"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:453
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:483
msgid "Accessible (teal/orange)"
msgstr "Доступный (бирюзовый/оранжевый)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:403
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:410
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38
msgid "Action"
msgstr "Действие"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:946
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977
msgid "Administrator access is required to disable logging."
msgstr "Для отключения журналирования требуются права администратора."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:934
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:965
msgid "Administrator access is required to enable logging."
msgstr "Для включения журналирования требуются права администратора."
@@ -69,7 +69,7 @@ msgstr "Для включения журналирования требуютс
msgid "Allow/deny rules, LAN logging, or anything else."
msgstr "Правила allow/deny, журналирование LAN и любые другие настройки."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1906
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1937
msgid "Also seen"
msgstr "Также встречаются"
@@ -81,16 +81,16 @@ msgstr "В брандмауэре уже есть другое незафикс
msgid "Another change overwrote WAN logging after it was saved; check the current state."
msgstr "Другое изменение перезаписало журналирование WAN после сохранения; проверьте текущее состояние."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2755
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2792
msgid "Any action"
msgstr "Любое действие"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1899
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1930
msgid "Any protocol"
msgstr "Любой протокол"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1404
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1476
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1435
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507
msgid "Auto"
msgstr "Авто"
@@ -106,39 +106,35 @@ msgstr "Невозможно включить журналирование, по
msgid "Changes:"
msgstr "Изменения:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:452
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:482
msgid "Classic (green/red)"
msgstr "Классический (зелёный/красный)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2467
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2504
msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr "Классический — зелёный/красный; доступный — бирюзовый/оранжевый"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:157
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:159
msgid "Clear all"
msgstr "Очистить всё"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2827
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2864
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+щелчок по правилу для настроек МЭ · в Простом виде щёлкните строку для полного сообщения"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2542
msgid "Click a row (Time or other non-link cells) to see the full log line (Simple view)."
msgstr "Щёлкните строку (Время или другие ячейки без ссылки), чтобы увидеть полную строку журнала (Простой вид)."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:159
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:183
msgid "Click a row for the full message"
msgstr "Щёлкните строку для полного сообщения"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2547
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2584
msgid "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a chip) to exclude."
msgstr "Щёлкните по IP, действию или протоколу для фильтрации; для исключения используйте меню «Протокол» (или ≠ на чипе)."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1900
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1931
msgid "Common"
msgstr "Частые"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1710
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1741
msgid "Connection lost — retrying…"
msgstr "Соединение потеряно — повторная попытка…"
@@ -150,43 +146,43 @@ msgstr "Не удалось получить блокировку журнали
msgid "Could not clear the WAN zone log option."
msgstr "Не удалось очистить параметр журнала зоны WAN."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:757
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:788
msgid "Could not create a temporary file for rule names."
msgstr "Не удалось создать временный файл для имён правил."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:944
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:975
msgid "Could not disable logging."
msgstr "Не удалось отключить журналирование."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:926
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:957
msgid "Could not enable logging."
msgstr "Не удалось включить журналирование."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:860
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:891
msgid "Could not load logging status."
msgstr "Не удалось загрузить состояние журналирования."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:763
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:794
msgid "Could not load rule names."
msgstr "Не удалось загрузить имена правил."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:759
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:790
msgid "Could not process firewall rule names."
msgstr "Не удалось обработать имена правил межсетевого экрана."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:761
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:792
msgid "Could not read the active firewall rules."
msgstr "Не удалось прочитать действующие правила межсетевого экрана."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:859
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:890
msgid "Could not refresh logging status; showing the last known state."
msgstr "Не удалось обновить состояние журналирования; показан последний известный статус."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:768
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:799
msgid "Could not refresh rule names."
msgstr "Не удалось обновить имена правил."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:766
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:797
msgid "Could not refresh rule names; previously loaded names are still shown."
msgstr "Не удалось обновить имена правил; отображаются ранее загруженные имена."
@@ -206,7 +202,7 @@ msgstr "Не удалось безопасно отследить изменен
msgid "Could not write the WAN zone log option."
msgstr "Не удалось записать параметр журнала зоны WAN."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2782
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr "Свой протокол (! для исключения). Переопределяет меню, если задан."
@@ -214,28 +210,28 @@ msgstr "Свой протокол (! для исключения). Переоп
msgid "DPort"
msgstr "Порт назн."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:407
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:414
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46
msgid "Destination"
msgstr "Назначение"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2812
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2849
msgid "Destination IP contains (! to exclude)"
msgstr "IP назначения содержит (! для исключения)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:409
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:416
msgid "Destination port"
msgstr "Порт назначения"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2817
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2854
msgid "Destination port (! to exclude)"
msgstr "Порт назначения (! для исключения)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2670
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2707
msgid "Detail"
msgstr "Подробно"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2660
msgid "Details"
msgstr "Подробности"
@@ -247,15 +243,15 @@ msgstr "Напр."
msgid "Disabling…"
msgstr "Отключение…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1453
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1484
msgid "Display limited to %d rows on this device; the larger Limit still buffers rows."
msgstr "Отображение ограничено %d строками на этом устройстве; больший лимит по-прежнему буферизует строки."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2408
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2445
msgid "Display options"
msgstr "Параметры отображения"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2501
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2538
msgid "Display options set Limit, Fetch budget, Maximum raw lines, row tint, palette, and hostnames."
msgstr "Параметры отображения задают лимит, бюджет выборки, максимум сырых строк, цвет строк, палитру и имена узлов."
@@ -275,7 +271,7 @@ msgstr "Включить журналирование drop/reject для WAN-з
msgid "Enable logging"
msgstr "Включить журналирование"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2496
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2533
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 "«Включить журналирование» включает только журналирование drop/reject для WAN-зоны (как в Сеть → Межсетевой экран). Оно не добавляет правил и не журналирует обычный LAN-трафик."
@@ -283,19 +279,19 @@ msgstr "«Включить журналирование» включает то
msgid "Enabling…"
msgstr "Включение…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1913
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1944
msgid "Exclude"
msgstr "Исключить"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:83
msgid "Exclude instead"
msgstr "Исключить вместо этого"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2422
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2459
msgid "Fetch budget"
msgstr "Бюджет выборки"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2511
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548
msgid "Fetch budget controls raw log lines per poll. Auto derives from Limit; Manual selects a bounded maximum."
msgstr "Бюджет выборки задаёт число сырых строк журнала за опрос. «Авто» выводится из лимита; «Вручную» задаёт ограниченный максимум."
@@ -309,12 +305,12 @@ msgstr "Фильтр по %s"
msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings."
msgstr "Фильтр журналов по правилу (подсказка: %s). Ctrl+клик открывает настройки файрвола."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2585
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2622
#: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View"
msgstr "Просмотр файрвола в реальном времени"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1677
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1708
msgid "Firewall log has no current entries; showing rows last received %s ago."
msgstr "В журнале межсетевого экрана сейчас нет записей; показаны строки, полученные %s назад."
@@ -326,11 +322,11 @@ msgstr "Флаги"
msgid "Flow"
msgstr "Поток"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2521
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2558
msgid "For a responsive table on a weak device, keep Limit at 250 rows or below. The weak-device cap affects rendered rows; the buffer can still retain more."
msgstr "Для отзывчивой таблицы на слабом устройстве установите Лимит не выше 250 строк. Ограничение слабого устройства влияет на отображаемые строки; буфер всё ещё может хранить больше."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:64
msgid "Forwarded"
msgstr "Транзитный"
@@ -338,19 +334,23 @@ msgstr "Транзитный"
msgid "Grant access to firewall live log view"
msgstr "Предоставить доступ к живому просмотру журнала межсетевого экрана"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2487
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2524
msgid "Help"
msgstr "Справка"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:151
msgid "Hide full message"
msgstr "Скрыть полное сообщение"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582
msgid "Hide rows"
msgstr "Скрыть строки"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1449
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1480
msgid "Hostname lookup paused while the router is under load."
msgstr "Разрешение имён приостановлено из-за нагрузки на маршрутизатор."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1904
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1935
msgid "ICMPv6"
msgstr "ICMPv6"
@@ -358,7 +358,7 @@ msgstr "ICMPv6"
msgid "IN"
msgstr "ВХОД"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2558
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2595
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-переменные success/error или info/warn; fwlive использует локальные цвета (работает автономно, данные не покидают устройство)."
@@ -366,11 +366,15 @@ msgstr "Если цветовая маркировка строк отсутст
msgid "If the WAN is quiet, wait for probes or use the optional ping check in Help / the enabling-logs guide."
msgstr "Если WAN неактивен, дождитесь проб или используйте необязательную проверку ping в Справке / руководстве по включению журналов."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:60
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2579
msgid "In Simple view, activate the message button or click a row to show or hide the full log line."
msgstr "В простом виде активируйте кнопку сообщения или нажмите на строку, чтобы показать или скрыть полную строку журнала."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62
msgid "Inbound"
msgstr "Входящий"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:83
msgid "Include instead"
msgstr "Включить вместо этого"
@@ -378,16 +382,16 @@ msgstr "Включить вместо этого"
msgid "Install kmod-nf-log and kmod-nf-log6 with the command for your OpenWrt release, then reload the firewall."
msgstr "Установите kmod-nf-log и kmod-nf-log6 командой для вашей версии OpenWrt, затем перезагрузите файрвол."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1708
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1739
msgid "Installation is incomplete. Reinstall luci-app-fwlive."
msgstr "Установка не завершена. Переустановите luci-app-fwlive."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:404
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:411
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40
msgid "Interface"
msgstr "Интерфейс"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2797
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2834
msgid "Interface (prefix ! to exclude)"
msgstr "Интерфейс (! в начале для исключения)"
@@ -404,11 +408,11 @@ msgstr "Отсутствуют модули журналирования ядр
msgid "Len"
msgstr "Длина"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2411
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2448
msgid "Limit"
msgstr "Лимит"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:814
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:845
msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr "Живой просмотр может быть неполным — зарегистрирована устаревшая таблица iptables"
@@ -416,16 +420,16 @@ msgstr "Живой просмотр может быть неполным — з
msgid "Logging is off on this router"
msgstr "Журналирование на этом роутере отключено"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1404
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1477
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1435
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1508
msgid "Manual"
msgstr "Вручную"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2440
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2477
msgid "Manual still uses server protection and poll cadence"
msgstr "«Вручную» по-прежнему использует защиту сервера и частоту опроса"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2516
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2553
msgid "Manual still uses server protection and poll cadence; it changes the fetch budget, not the polling interval."
msgstr "«Вручную» по-прежнему использует защиту сервера и частоту опроса; меняется бюджет выборки, а не интервал опроса."
@@ -433,18 +437,18 @@ msgstr "«Вручную» по-прежнему использует защит
msgid "Manual test (System → Terminal):"
msgstr "Ручной тест (Система → Терминал):"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2434
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2471
msgid "Maximum raw lines"
msgstr "Максимум сырых строк"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:52
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:340
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2682
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2689
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:368
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2719
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2726
msgid "Message"
msgstr "Сообщение"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2792
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2829
msgid "More filters"
msgstr "Больше фильтров"
@@ -476,7 +480,7 @@ msgstr "Ничего не изменится, пока вы не нажмёте
msgid "OUT"
msgstr "ВЫХОД"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2712
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2749
msgid "One line"
msgstr "Одна строка"
@@ -496,57 +500,57 @@ msgstr "OpenWrt 25.12 и новее (apk):"
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 не записывает события межсетевого экрана в журнал, пока не включено журналирование. Живой просмотр показывает только то, что firewall уже журналирует — он не добавляет правила allow/deny."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:61
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:63
msgid "Outbound"
msgstr "Исходящий"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2461
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2498
msgid "Palette"
msgstr "Палитра"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2636
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2673
msgid "Pause"
msgstr "Пауза"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1770
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801
msgid "Paused"
msgstr "Пауза"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1436
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1467
msgid "Polling slowed: the router is responding slowly, so polls run less often."
msgstr "Опрос замедлен: маршрутизатор отвечает медленно, поэтому опросы выполняются реже."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:405
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:412
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44
msgid "Proto"
msgstr "Протокол"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2773
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2810
msgid "Protocol — common values"
msgstr "Протокол — частые значения"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2752
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2789
msgid "Quick search"
msgstr "Быстрый поиск"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:118
msgid "Remove filter"
msgstr "Удалить фильтр"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802
msgid "Resume"
msgstr "Продолжить"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2451
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2488
msgid "Row tint"
msgstr "Цвет строк"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2589
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, когда включена. Выберите классический (зелёный/красный, по умолчанию) или доступный (бирюзовый/оранжевый). Цвет текста действия сохраняется в любом случае."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2608
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2645
msgid "Row tint used a local color fallback because the active LuCI theme did not apply pass/deny backgrounds."
msgstr "Цветовая маркировка строк использовала локальный запасной цвет, так как активная тема LuCI не применяет фон пропуска/отказа."
@@ -554,19 +558,19 @@ msgstr "Цветовая маркировка строк использовал
msgid "Rule"
msgstr "Правило"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2660
msgid "Rule name details"
msgstr "Подробности об именах правил"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:785
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:816
msgid "Rule-name lookup does not change your firewall rules."
msgstr "Получение имён правил не изменяет правила межсетевого экрана."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:770
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:801
msgid "Rule-name lookup failed."
msgstr "Не удалось получить имена правил."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:777
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
msgid "Rule-name lookup reached a safety limit."
msgstr "При загрузке имён правил достигнут защитный лимит."
@@ -574,93 +578,97 @@ msgstr "При загрузке имён правил достигнут защ
msgid "SPort"
msgstr "Порт ист."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:402
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:409
msgid "Search"
msgstr "Поиск"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1433
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1464
msgid "Server adaptive protection is disabled."
msgstr "Адаптивная защита сервера отключена."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1441
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1472
msgid "Server limited fetch: the router is under load; at most %d log lines per poll."
msgstr "Сервер ограничил выборку: маршрутизатор нагружен; не более %d строк журнала за опрос."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1446
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1477
msgid "Server limited fetch: this poll returned fewer lines to protect the router."
msgstr "Сервер ограничил выборку: этот опрос вернул меньше строк для защиты маршрутизатора."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1431
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1462
msgid "Server protection state is unknown."
msgstr "Состояние защиты сервера неизвестно."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2478
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:151
msgid "Show full message"
msgstr "Показать полное сообщение"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2515
msgid "Show hostnames"
msgstr "Показывать имена узлов"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2449
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2486
msgid "Show pass/deny row background colors"
msgstr "Показывать цвета фона строк pass/deny"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2772
msgid "Show rows"
msgstr "Показать строки"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1511
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1542
msgid "Showing a compact summary of the top of the shown sample."
msgstr "Показана краткая сводка верхней части отображаемой выборки."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2659
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2696
msgid "Simple"
msgstr "Простой"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:788
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:819
msgid "Some entries may show names derived from their log prefixes. Searching by a friendly rule name may miss those entries."
msgstr "Некоторые записи могут показывать имена, полученные из префиксов журнала. Поиск по понятному имени правила может не найти такие записи."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:839
msgid "Some rule names may be missing"
msgstr "Некоторые имена правил могут отсутствовать"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:406
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:413
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45
msgid "Source"
msgstr "Источник"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2802
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2839
msgid "Source IP contains (! to exclude)"
msgstr "IP источника содержит (! для исключения)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:408
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:415
msgid "Source port"
msgstr "Порт источника"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2807
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2844
msgid "Source port (! to exclude)"
msgstr "Порт источника (! для исключения)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1513
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1544
msgid "Summary data is unavailable; show rows to inspect the sample."
msgstr "Данные сводки недоступны; покажите строки для проверки выборки."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2726
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2763
msgid "Summary mode"
msgstr "Режим сводки"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2506
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2543
msgid "Summary mode: when the router responds slowly, a compact summary replaces the table. Use Show rows to see the table; it returns automatically after three fast polls."
msgstr "Режим сводки: если маршрутизатор отвечает медленно, вместо таблицы показывается краткая сводка. Нажмите «Показать строки», чтобы увидеть таблицу; она вернётся автоматически после трёх быстрых опросов."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1546
msgid "Summary was bounded before all categories could be included."
msgstr "Сводка ограничена до включения всех категорий."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2526
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563
msgid "Switching tabs pauses polling; returning performs one catch-up poll. Hostnames are off by default because lookups add work."
msgstr "При переключении вкладки опрос приостанавливается; при возврате выполняется один догоняющий опрос. Имена хостов по умолчанию отключены, поскольку их разрешение создаёт дополнительную нагрузку."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1606
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1637
msgid "Table refresh throttled: the event rate is high, so the table repaints less often to protect the browser. The buffer still updates; refresh resumes automatically."
msgstr "Обновление таблицы ограничено: частота событий высокая, поэтому таблица перерисовывается реже для защиты браузера. Буфер продолжает обновляться; обновление возобновится автоматически."
@@ -668,23 +676,23 @@ msgstr "Обновление таблицы ограничено: частота
msgid "The firewall did not reload; saved and live logging may differ."
msgstr "Брандмауэр не перезагрузился; сохранённые и активные настройки журналирования могут различаться."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2536
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2573
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 сам лимит не задаёт."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1709
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1740
msgid "The router could not read the firewall log — retrying…"
msgstr "Маршрутизатор не смог прочитать журнал межсетевого экрана — повторная попытка…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2531
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2568
msgid "The router log ring may evict older events before fwlive reads them. fwlive cannot recover evicted entries or change forwarding behavior."
msgstr "Кольцевой журнал маршрутизатора может вытеснить старые события до того, как fwlive их прочитает. fwlive не может восстановить вытесненные записи и не изменяет пересылку."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2491
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2528
msgid "The table updates automatically when your firewall logs traffic. Use Pause if it moves too fast."
msgstr "Таблица обновляется автоматически, когда файрвол пишет в журнал. Используйте Паузу, если слишком быстро."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2611
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648
msgid "Theme tint fallback"
msgstr "Запасной цвет темы"
@@ -700,15 +708,15 @@ msgstr "Этот маршрутизатор сообщил о блоке жур
msgid "Time"
msgstr "Время"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1520
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
msgid "Top drops"
msgstr "Частые сбросы"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1523
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1554
msgid "Top rules"
msgstr "Частые правила"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1548
msgid "Top talkers"
msgstr "Частые источники"
@@ -728,16 +736,16 @@ msgstr "Включает журналирование для WAN-зоны меж
msgid "Undo:"
msgstr "Отмена:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2555
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2592
msgid "Use Detail for all columns (flags, length, raw message)."
msgstr "Используйте «Подробно» для всех столбцов (флаги, длина, исходное сообщение)."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2641
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2647
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2678
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684
msgid "View"
msgstr "Вид"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:976
msgid "WAN drop/reject logging is off."
msgstr "Журналирование drop/reject для WAN отключено."
@@ -745,11 +753,11 @@ msgstr "Журналирование drop/reject для WAN отключено."
msgid "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up here. Normal LAN browsing will not."
msgstr "Журналирование drop/reject для WAN включено. Заблокированный входящий WAN-трафик будет отображаться здесь. Обычный LAN-трафик — нет."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:930
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:961
msgid "WAN drop/reject logging is on. Blocked inbound traffic should appear here as it happens — not normal LAN browsing."
msgstr "Журналирование drop/reject для WAN включено. Заблокированный входящий трафик должен появляться здесь по мере поступления — обычный LAN-трафик не отображается."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:932
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:963
msgid "WAN logging is already enabled."
msgstr "WAN-журналирование уже включено."
@@ -778,25 +786,25 @@ msgstr "WAN-журналирование недоступно: нет зоны W
msgid "Waiting for firewall events"
msgstr "Ожидание событий межсетевого экрана"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1770
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2636
msgid "Watching"
msgstr "Наблюдение"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2701
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2738
msgid "Wrap"
msgstr "Перенос"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:72
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2757
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794
msgid "block"
msgstr "заблокирован"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1255
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1286
msgid "buffer filling"
msgstr "буфер заполняется"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1259
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1290
msgid "buffer full"
msgstr "буфер заполнен"
@@ -808,7 +816,7 @@ msgstr "содержит"
msgid "default 10/minute"
msgstr "по умолчанию 10/мин"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1264
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1295
msgid "display limited to %d rows"
msgstr "отображение ограничено %d строками"
@@ -816,12 +824,12 @@ msgstr "отображение ограничено %d строками"
msgid "does not contain"
msgstr "не содержит"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2758
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:75
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2795
msgid "drop"
msgstr "отброшен"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1268
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1299
msgid "hostname lookup paused"
msgstr "разрешение имён приостановлено"
@@ -829,16 +837,16 @@ msgstr "разрешение имён приостановлено"
msgid "is"
msgstr "есть"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1254
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1285
msgid "loading buffer"
msgstr "загрузка буфера"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1695
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1726
msgid "loading…"
msgstr "загрузка…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1493
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1524
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1533
msgid "none"
msgstr "нет"
@@ -846,101 +854,104 @@ msgstr "нет"
msgid "not"
msgstr "не"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1921
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1952
msgid "not AH"
msgstr "не AH"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1920
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1951
msgid "not ESP"
msgstr "не ESP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1919
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1950
msgid "not GRE"
msgstr "не GRE"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1916
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1947
msgid "not ICMP"
msgstr "не ICMP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1917
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1948
msgid "not ICMPv6"
msgstr "не ICMPv6"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1918
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1949
msgid "not IGMP"
msgstr "не IGMP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1922
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1953
msgid "not SCTP"
msgstr "не SCTP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1914
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1945
msgid "not TCP"
msgstr "не TCP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1915
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1946
msgid "not UDP"
msgstr "не UDP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2763
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2800
msgid "not block"
msgstr "не блокировать"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2762
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2799
msgid "not drop"
msgstr "не отбрасывать"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2761
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2798
msgid "not pass"
msgstr "не пропускать"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2764
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2801
msgid "not reject"
msgstr "не отклонять"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2765
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2802
msgid "not unknown"
msgstr "не неизвестно"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2780
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2817
msgid "or type…"
msgstr "или введите…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2756
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2793
msgid "pass"
msgstr "разрешён"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1266
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1297
msgid "polling slowed"
msgstr "опрос замедлен"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2759
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:76
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2796
msgid "reject"
msgstr "отклонён"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1270
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1301
msgid "scroll frozen — scroll to top to follow live"
msgstr "прокрутка заморожена — прокрутите вверх, чтобы следить в реальном времени"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1267
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1298
msgid "server limited fetch"
msgstr "сервер ограничил выборку"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1261
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1292
msgid "table refresh throttled"
msgstr "обновление таблицы ограничено"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:63
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:65
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2797
msgid "unknown"
msgstr "неизвестно"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:750
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:781
msgid "using fw4"
msgstr "используется fw4"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:176
msgid "· %s"
msgstr "· %s"
#~ msgid "Click a row (Time or other non-link cells) to see the full log line (Simple view)."
#~ msgstr "Щёлкните строку (Время или другие ячейки без ссылки), чтобы увидеть полную строку журнала (Простой вид)."
+187 -179
View File
@@ -1,58 +1,58 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1697
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1728
msgid "%d matching · %d/%d stored"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1662
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1693
msgid "%dd"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1661
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692
msgid "%dh"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1660
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1691
msgid "%dm"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1659
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690
msgid "%ds"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1423
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1454
msgid "%s · requested up to %d raw lines · %d firewall messages returned"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1413
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1444
msgid ""
"%s · requested up to %d raw lines · server limited fetch to %d · %d firewall "
"messages returned"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:550
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:580
msgid "%s: %s"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1699
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1730
msgid "0 matching · %d/%d stored"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:453
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:483
msgid "Accessible (teal/orange)"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:410
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:440
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38
msgid "Action"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:946
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977
msgid "Administrator access is required to disable logging."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:934
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:965
msgid "Administrator access is required to enable logging."
msgstr ""
@@ -60,7 +60,7 @@ msgstr ""
msgid "Allow/deny rules, LAN logging, or anything else."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1906
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1937
msgid "Also seen"
msgstr ""
@@ -74,16 +74,16 @@ msgid ""
"state."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2757
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2792
msgid "Any action"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1899
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1930
msgid "Any protocol"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1404
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1476
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1435
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507
msgid "Auto"
msgstr ""
@@ -99,45 +99,39 @@ msgstr ""
msgid "Changes:"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:452
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:482
msgid "Classic (green/red)"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2469
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2504
msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:157
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:159
msgid "Clear all"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2829
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2864
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 ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2544
msgid ""
"Click a row (Time or other non-link cells) to see the full log line (Simple "
"view)."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:159
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:183
msgid "Click a row for the full message"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2549
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2584
msgid ""
"Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a "
"chip) to exclude."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1900
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1931
msgid "Common"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1710
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1741
msgid "Connection lost — retrying…"
msgstr ""
@@ -149,43 +143,43 @@ msgstr ""
msgid "Could not clear the WAN zone log option."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:757
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:788
msgid "Could not create a temporary file for rule names."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:944
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:975
msgid "Could not disable logging."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:926
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:957
msgid "Could not enable logging."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:860
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:891
msgid "Could not load logging status."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:763
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:794
msgid "Could not load rule names."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:759
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:790
msgid "Could not process firewall rule names."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:761
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:792
msgid "Could not read the active firewall rules."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:859
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:890
msgid "Could not refresh logging status; showing the last known state."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:768
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:799
msgid "Could not refresh rule names."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:766
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:797
msgid "Could not refresh rule names; previously loaded names are still shown."
msgstr ""
@@ -205,7 +199,7 @@ msgstr ""
msgid "Could not write the WAN zone log option."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2784
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr ""
@@ -213,28 +207,28 @@ msgstr ""
msgid "DPort"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:414
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:444
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46
msgid "Destination"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2814
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2849
msgid "Destination IP contains (! to exclude)"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:416
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:446
msgid "Destination port"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2854
msgid "Destination port (! to exclude)"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2672
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2707
msgid "Detail"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2625
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2660
msgid "Details"
msgstr ""
@@ -246,17 +240,17 @@ msgstr ""
msgid "Disabling…"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1453
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1484
msgid ""
"Display limited to %d rows on this device; the larger Limit still buffers "
"rows."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2410
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2445
msgid "Display options"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2503
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2538
msgid ""
"Display options set Limit, Fetch budget, Maximum raw lines, row tint, "
"palette, and hostnames."
@@ -278,7 +272,7 @@ msgstr ""
msgid "Enable logging"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2498
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2533
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."
@@ -288,19 +282,19 @@ msgstr ""
msgid "Enabling…"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1913
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1944
msgid "Exclude"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:83
msgid "Exclude instead"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2424
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2459
msgid "Fetch budget"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2513
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548
msgid ""
"Fetch budget controls raw log lines per poll. Auto derives from Limit; "
"Manual selects a bounded maximum."
@@ -316,12 +310,12 @@ msgstr ""
msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2587
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2622
#: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1677
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1708
msgid "Firewall log has no current entries; showing rows last received %s ago."
msgstr ""
@@ -333,13 +327,13 @@ msgstr ""
msgid "Flow"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2558
msgid ""
"For a responsive table on a weak device, keep Limit at 250 rows or below. "
"The weak-device cap affects rendered rows; the buffer can still retain more."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:64
msgid "Forwarded"
msgstr ""
@@ -347,19 +341,23 @@ msgstr ""
msgid "Grant access to firewall live log view"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2489
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2524
msgid "Help"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:151
msgid "Hide full message"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582
msgid "Hide rows"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1449
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1480
msgid "Hostname lookup paused while the router is under load."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1904
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1935
msgid "ICMPv6"
msgstr ""
@@ -367,7 +365,7 @@ msgstr ""
msgid "IN"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2560
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2595
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 "
@@ -380,11 +378,17 @@ msgid ""
"Help / the enabling-logs guide."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:60
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2579
msgid ""
"In Simple view, activate the message button or click a row to show or hide "
"the full log line."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62
msgid "Inbound"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:83
msgid "Include instead"
msgstr ""
@@ -394,16 +398,16 @@ msgid ""
"release, then reload the firewall."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1708
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1739
msgid "Installation is incomplete. Reinstall luci-app-fwlive."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:411
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:441
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40
msgid "Interface"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2799
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2834
msgid "Interface (prefix ! to exclude)"
msgstr ""
@@ -420,11 +424,11 @@ msgstr ""
msgid "Len"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2413
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2448
msgid "Limit"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:814
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:845
msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr ""
@@ -432,16 +436,16 @@ msgstr ""
msgid "Logging is off on this router"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1404
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1477
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1435
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1508
msgid "Manual"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2442
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2477
msgid "Manual still uses server protection and poll cadence"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2518
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2553
msgid ""
"Manual still uses server protection and poll cadence; it changes the fetch "
"budget, not the polling interval."
@@ -451,18 +455,18 @@ msgstr ""
msgid "Manual test (System → Terminal):"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2436
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2471
msgid "Maximum raw lines"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:52
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:340
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2691
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:368
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2719
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2726
msgid "Message"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2829
msgid "More filters"
msgstr ""
@@ -495,7 +499,7 @@ msgstr ""
msgid "OUT"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2714
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2749
msgid "One line"
msgstr ""
@@ -518,61 +522,61 @@ msgid ""
"deny rules."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:61
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:63
msgid "Outbound"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2463
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2498
msgid "Palette"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2673
msgid "Pause"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1770
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801
msgid "Paused"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1436
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1467
msgid ""
"Polling slowed: the router is responding slowly, so polls run less often."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:412
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:442
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44
msgid "Proto"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2775
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2810
msgid "Protocol — common values"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2754
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2789
msgid "Quick search"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:118
msgid "Remove filter"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802
msgid "Resume"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2453
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2488
msgid "Row tint"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2554
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2589
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 ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2610
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2645
msgid ""
"Row tint used a local color fallback because the active LuCI theme did not "
"apply pass/deny backgrounds."
@@ -582,19 +586,19 @@ msgstr ""
msgid "Rule"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2625
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2660
msgid "Rule name details"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:785
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:816
msgid "Rule-name lookup does not change your firewall rules."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:770
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:801
msgid "Rule-name lookup failed."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:777
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
msgid "Rule-name lookup reached a safety limit."
msgstr ""
@@ -602,103 +606,107 @@ msgstr ""
msgid "SPort"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:409
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:439
msgid "Search"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1433
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1464
msgid "Server adaptive protection is disabled."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1441
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1472
msgid ""
"Server limited fetch: the router is under load; at most %d log lines per "
"poll."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1446
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1477
msgid ""
"Server limited fetch: this poll returned fewer lines to protect the router."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1431
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1462
msgid "Server protection state is unknown."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2480
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:151
msgid "Show full message"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2515
msgid "Show hostnames"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2451
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2486
msgid "Show pass/deny row background colors"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2737
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2772
msgid "Show rows"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1511
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1542
msgid "Showing a compact summary of the top of the shown sample."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2661
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2696
msgid "Simple"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:788
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:819
msgid ""
"Some entries may show names derived from their log prefixes. Searching by a "
"friendly rule name may miss those entries."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:839
msgid "Some rule names may be missing"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:413
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:443
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45
msgid "Source"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2804
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2839
msgid "Source IP contains (! to exclude)"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:415
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:445
msgid "Source port"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2809
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2844
msgid "Source port (! to exclude)"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1513
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1544
msgid "Summary data is unavailable; show rows to inspect the sample."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2728
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2763
msgid "Summary mode"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2508
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2543
msgid ""
"Summary mode: when the router responds slowly, a compact summary replaces "
"the table. Use Show rows to see the table; it returns automatically after "
"three fast polls."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1546
msgid "Summary was bounded before all categories could be included."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2528
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563
msgid ""
"Switching tabs pauses polling; returning performs one catch-up poll. "
"Hostnames are off by default because lookups add work."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1606
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1637
msgid ""
"Table refresh throttled: the event rate is high, so the table repaints less "
"often to protect the browser. The buffer still updates; refresh resumes "
@@ -709,30 +717,30 @@ msgstr ""
msgid "The firewall did not reload; saved and live logging may differ."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2538
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2573
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 ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1709
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1740
msgid "The router could not read the firewall log — retrying…"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2533
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2568
msgid ""
"The router log ring may evict older events before fwlive reads them. fwlive "
"cannot recover evicted entries or change forwarding behavior."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2493
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2528
msgid ""
"The table updates automatically when your firewall logs traffic. Use Pause "
"if it moves too fast."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2613
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648
msgid "Theme tint fallback"
msgstr ""
@@ -749,15 +757,15 @@ msgstr ""
msgid "Time"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1520
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
msgid "Top drops"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1523
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1554
msgid "Top rules"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1548
msgid "Top talkers"
msgstr ""
@@ -780,16 +788,16 @@ msgstr ""
msgid "Undo:"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2557
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2592
msgid "Use Detail for all columns (flags, length, raw message)."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2649
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2678
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684
msgid "View"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:976
msgid "WAN drop/reject logging is off."
msgstr ""
@@ -799,13 +807,13 @@ msgid ""
"here. Normal LAN browsing will not."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:930
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:961
msgid ""
"WAN drop/reject logging is on. Blocked inbound traffic should appear here as "
"it happens — not normal LAN browsing."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:932
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:963
msgid "WAN logging is already enabled."
msgstr ""
@@ -834,25 +842,25 @@ msgstr ""
msgid "Waiting for firewall events"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1770
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2601
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2636
msgid "Watching"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2703
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2738
msgid "Wrap"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:72
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2759
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794
msgid "block"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1255
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1286
msgid "buffer filling"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1259
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1290
msgid "buffer full"
msgstr ""
@@ -864,7 +872,7 @@ msgstr ""
msgid "default 10/minute"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1264
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1295
msgid "display limited to %d rows"
msgstr ""
@@ -872,12 +880,12 @@ msgstr ""
msgid "does not contain"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:75
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2795
msgid "drop"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1268
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1299
msgid "hostname lookup paused"
msgstr ""
@@ -885,16 +893,16 @@ msgstr ""
msgid "is"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1254
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1285
msgid "loading buffer"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1695
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1726
msgid "loading…"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1493
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1524
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1533
msgid "none"
msgstr ""
@@ -902,98 +910,98 @@ msgstr ""
msgid "not"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1921
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1952
msgid "not AH"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1920
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1951
msgid "not ESP"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1919
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1950
msgid "not GRE"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1916
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1947
msgid "not ICMP"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1917
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1948
msgid "not ICMPv6"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1918
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1949
msgid "not IGMP"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1922
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1953
msgid "not SCTP"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1914
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1945
msgid "not TCP"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1915
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1946
msgid "not UDP"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2765
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2800
msgid "not block"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2764
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2799
msgid "not drop"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2763
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2798
msgid "not pass"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2766
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2801
msgid "not reject"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2767
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2802
msgid "not unknown"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2782
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2817
msgid "or type…"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2758
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2793
msgid "pass"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1266
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1297
msgid "polling slowed"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2761
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:76
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2796
msgid "reject"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1270
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1301
msgid "scroll frozen — scroll to top to follow live"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1267
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1298
msgid "server limited fetch"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1261
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1292
msgid "table refresh throttled"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:63
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2762
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:65
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2797
msgid "unknown"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:750
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:781
msgid "using fw4"
msgstr ""
+188 -177
View File
@@ -12,56 +12,56 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1697
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1728
msgid "%d matching · %d/%d stored"
msgstr "%d 条匹配 · 已存 %d/%d"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1662
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1693
msgid "%dd"
msgstr "%d天"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1661
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692
msgid "%dh"
msgstr "%d小时"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1660
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1691
msgid "%dm"
msgstr "%d分钟"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1659
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690
msgid "%ds"
msgstr "%d秒"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1423
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1454
msgid "%s · requested up to %d raw lines · %d firewall messages returned"
msgstr "%s · 请求最多 %d 行原始日志 · 返回 %d 条防火墙消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1413
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1444
msgid "%s · requested up to %d raw lines · server limited fetch to %d · %d firewall messages returned"
msgstr "%s · 请求最多 %d 行原始日志 · 服务器将抓取限制为 %d · 返回 %d 条防火墙消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:543
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:550
msgid "%s: %s"
msgstr "%s: %s"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1699
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1730
msgid "0 matching · %d/%d stored"
msgstr "0 条匹配 · 已存 %d/%d"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:453
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:483
msgid "Accessible (teal/orange)"
msgstr "无障碍(青绿/橙色)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:403
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:410
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38
msgid "Action"
msgstr "动作"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:946
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977
msgid "Administrator access is required to disable logging."
msgstr "需要管理员权限才能禁用日志记录。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:934
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:965
msgid "Administrator access is required to enable logging."
msgstr "需要管理员权限才能启用日志记录。"
@@ -69,7 +69,7 @@ msgstr "需要管理员权限才能启用日志记录。"
msgid "Allow/deny rules, LAN logging, or anything else."
msgstr "允许/拒绝规则、LAN 日志记录或任何其他设置。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1906
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1937
msgid "Also seen"
msgstr "其他常见"
@@ -81,16 +81,16 @@ msgstr "防火墙另有未提交的更改;请先应用或还原。"
msgid "Another change overwrote WAN logging after it was saved; check the current state."
msgstr "保存后另有更改覆盖了 WAN 日志设置;请检查当前状态。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2755
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2792
msgid "Any action"
msgstr "所有动作"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1899
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1930
msgid "Any protocol"
msgstr "任意协议"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1404
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1476
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1435
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507
msgid "Auto"
msgstr "自动"
@@ -106,39 +106,35 @@ msgstr "无法启用日志记录,直到内核日志模块已安装。"
msgid "Changes:"
msgstr "变更:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:452
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:482
msgid "Classic (green/red)"
msgstr "经典(绿/红)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2467
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2504
msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr "经典使用绿/红;无障碍使用青绿/橙色"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:157
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:159
msgid "Clear all"
msgstr "全部清除"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2827
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2864
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+点击规则打开防火墙设置 · 在简单视图中点击行查看完整消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2542
msgid "Click a row (Time or other non-link cells) to see the full log line (Simple view)."
msgstr "点击一行(时间或其他非链接单元格)可查看完整日志行(简单视图)。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:159
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:183
msgid "Click a row for the full message"
msgstr "点击行可查看完整消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2547
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2584
msgid "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a chip) to exclude."
msgstr "点击 IP、动作或协议进行筛选;使用协议菜单(或筛选标签上的 ≠)进行排除。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1900
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1931
msgid "Common"
msgstr "常用"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1710
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1741
msgid "Connection lost — retrying…"
msgstr "连接已断开 — 正在重试…"
@@ -150,43 +146,43 @@ msgstr "无法获取日志记录锁。"
msgid "Could not clear the WAN zone log option."
msgstr "无法清除 WAN 区域日志选项。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:757
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:788
msgid "Could not create a temporary file for rule names."
msgstr "无法为规则名称创建临时文件。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:944
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:975
msgid "Could not disable logging."
msgstr "无法禁用日志记录。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:926
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:957
msgid "Could not enable logging."
msgstr "无法启用日志记录。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:860
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:891
msgid "Could not load logging status."
msgstr "无法加载日志状态。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:763
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:794
msgid "Could not load rule names."
msgstr "无法加载规则名称。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:759
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:790
msgid "Could not process firewall rule names."
msgstr "无法处理防火墙规则名称。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:761
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:792
msgid "Could not read the active firewall rules."
msgstr "无法读取当前生效的防火墙规则。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:859
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:890
msgid "Could not refresh logging status; showing the last known state."
msgstr "无法刷新日志状态;显示上次已知状态。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:768
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:799
msgid "Could not refresh rule names."
msgstr "无法刷新规则名称。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:766
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:797
msgid "Could not refresh rule names; previously loaded names are still shown."
msgstr "无法刷新规则名称;仍显示之前加载的名称。"
@@ -206,7 +202,7 @@ msgstr "无法安全跟踪日志记录的更改;日志记录未更改。"
msgid "Could not write the WAN zone log option."
msgstr "无法写入 WAN 区域日志选项。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2782
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr "自定义协议(! 排除)。有内容时覆盖菜单。"
@@ -214,28 +210,28 @@ msgstr "自定义协议(! 排除)。有内容时覆盖菜单。"
msgid "DPort"
msgstr "目的端口"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:407
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:414
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:46
msgid "Destination"
msgstr "目的地址"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2812
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2849
msgid "Destination IP contains (! to exclude)"
msgstr "目的 IP 包含(! 表示排除)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:409
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:416
msgid "Destination port"
msgstr "目标端口"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2817
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2854
msgid "Destination port (! to exclude)"
msgstr "目的端口(! 表示排除)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2670
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2707
msgid "Detail"
msgstr "详细"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2660
msgid "Details"
msgstr "详情"
@@ -247,15 +243,15 @@ msgstr "方向"
msgid "Disabling…"
msgstr "正在禁用…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1453
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1484
msgid "Display limited to %d rows on this device; the larger Limit still buffers rows."
msgstr "显示上限为 %d 行(本设备);较大的行数限制仍会缓冲这些行。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2408
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2445
msgid "Display options"
msgstr "显示选项"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2501
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2538
msgid "Display options set Limit, Fetch budget, Maximum raw lines, row tint, palette, and hostnames."
msgstr "显示选项可设置行数限制、抓取配额、最大原始行数、行着色、配色和主机名。"
@@ -275,7 +271,7 @@ msgstr "启用 WAN 区域的丢弃/拒绝日志记录(同 网络 → 防火墙
msgid "Enable logging"
msgstr "启用日志"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2496
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2533
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 "启用日志仅开启 WAN 区域的丢弃/拒绝日志记录(同 网络 → 防火墙),不会添加规则,也不会记录正常的 LAN 浏览。"
@@ -283,19 +279,19 @@ msgstr "启用日志仅开启 WAN 区域的丢弃/拒绝日志记录(同 网
msgid "Enabling…"
msgstr "正在启用…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1913
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1944
msgid "Exclude"
msgstr "排除"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:83
msgid "Exclude instead"
msgstr "改为排除"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2422
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2459
msgid "Fetch budget"
msgstr "抓取配额"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2511
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548
msgid "Fetch budget controls raw log lines per poll. Auto derives from Limit; Manual selects a bounded maximum."
msgstr "抓取配额控制每次轮询的原始日志行数。「自动」由行数限制推导;「手动」可选择一个有上限的最大值。"
@@ -309,12 +305,12 @@ msgstr "按 %s 筛选"
msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings."
msgstr "按规则筛选日志(提示:%s)。Ctrl+点击可打开防火墙设置。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2585
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2622
#: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View"
msgstr "防火墙实时视图"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1677
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1708
msgid "Firewall log has no current entries; showing rows last received %s ago."
msgstr "防火墙日志当前没有条目;显示的是 %s 前收到的日志行。"
@@ -326,11 +322,11 @@ msgstr "标志"
msgid "Flow"
msgstr "流向"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2521
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2558
msgid "For a responsive table on a weak device, keep Limit at 250 rows or below. The weak-device cap affects rendered rows; the buffer can still retain more."
msgstr "要让弱设备上的表格保持响应,请将 行数限制 保持在 250 行或更少。弱设备限制影响渲染的行数;缓冲区仍可保留更多行。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:64
msgid "Forwarded"
msgstr "已转发"
@@ -338,19 +334,23 @@ msgstr "已转发"
msgid "Grant access to firewall live log view"
msgstr "授予防火墙实时日志视图的访问权限"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2487
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2524
msgid "Help"
msgstr "帮助"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:151
msgid "Hide full message"
msgstr "隐藏完整消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582
msgid "Hide rows"
msgstr "隐藏行"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1449
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1480
msgid "Hostname lookup paused while the router is under load."
msgstr "主机名解析已暂停:路由器负载较高。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1904
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1935
msgid "ICMPv6"
msgstr "ICMPv6"
@@ -358,7 +358,7 @@ msgstr "ICMPv6"
msgid "IN"
msgstr "IN"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2558
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2595
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 会回退到本地颜色(隔离运行,数据不会离开本设备)。"
@@ -366,11 +366,15 @@ msgstr "如果行着色看起来缺失,当前 LuCI 主题可能未提供成功
msgid "If the WAN is quiet, wait for probes or use the optional ping check in Help / the enabling-logs guide."
msgstr "如果 WAN 侧较安静,请等待探测报文,或使用「帮助」/启用日志指南中的可选 ping 检查。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:60
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2579
msgid "In Simple view, activate the message button or click a row to show or hide the full log line."
msgstr "在简洁视图中,激活消息按钮或单击行即可显示或隐藏完整的日志行。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62
msgid "Inbound"
msgstr "入站"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:83
msgid "Include instead"
msgstr "改为包含"
@@ -378,16 +382,16 @@ msgstr "改为包含"
msgid "Install kmod-nf-log and kmod-nf-log6 with the command for your OpenWrt release, then reload the firewall."
msgstr "请使用适用于您 OpenWrt 版本的命令安装 kmod-nf-log 和 kmod-nf-log6,然后重新加载防火墙。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1708
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1739
msgid "Installation is incomplete. Reinstall luci-app-fwlive."
msgstr "安装不完整。请重新安装 luci-app-fwlive。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:404
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:411
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:40
msgid "Interface"
msgstr "接口"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2797
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2834
msgid "Interface (prefix ! to exclude)"
msgstr "接口(前缀 ! 表示排除)"
@@ -404,11 +408,11 @@ msgstr "缺少内核日志模块"
msgid "Len"
msgstr "长度"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2411
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2448
msgid "Limit"
msgstr "行数限制"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:814
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:845
msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr "实时视图可能不完整 — 已注册旧版 iptables 表"
@@ -416,16 +420,16 @@ msgstr "实时视图可能不完整 — 已注册旧版 iptables 表"
msgid "Logging is off on this router"
msgstr "此路由器的日志记录已关闭"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1404
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1477
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1435
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1508
msgid "Manual"
msgstr "手动"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2440
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2477
msgid "Manual still uses server protection and poll cadence"
msgstr "手动模式仍受服务器保护和轮询节奏约束"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2516
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2553
msgid "Manual still uses server protection and poll cadence; it changes the fetch budget, not the polling interval."
msgstr "手动模式仍受服务器保护和轮询节奏约束;它改变的是抓取配额,而不是轮询间隔。"
@@ -433,18 +437,18 @@ msgstr "手动模式仍受服务器保护和轮询节奏约束;它改变的是
msgid "Manual test (System → Terminal):"
msgstr "手动测试(系统 → 终端):"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2434
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2471
msgid "Maximum raw lines"
msgstr "最大原始行数"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:52
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:340
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2682
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2689
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:368
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2719
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2726
msgid "Message"
msgstr "消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2792
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2829
msgid "More filters"
msgstr "更多筛选"
@@ -476,7 +480,7 @@ msgstr "点击「启用日志」之前不会有任何更改。"
msgid "OUT"
msgstr "出接口"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2712
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2749
msgid "One line"
msgstr "单行"
@@ -496,57 +500,57 @@ msgstr "OpenWrt 25.12 及更高版本(apk):"
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 不会将防火墙事件写入日志。实时视图只显示防火墙已记录的内容 — 它不会添加允许/拒绝规则。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:61
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:63
msgid "Outbound"
msgstr "出站"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2461
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2498
msgid "Palette"
msgstr "配色"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2636
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2673
msgid "Pause"
msgstr "暂停"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1770
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801
msgid "Paused"
msgstr "已暂停"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1436
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1467
msgid "Polling slowed: the router is responding slowly, so polls run less often."
msgstr "轮询已放慢:路由器响应较慢,因此降低轮询频率。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:405
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:412
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:44
msgid "Proto"
msgstr "协议"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2773
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2810
msgid "Protocol — common values"
msgstr "协议 — 常用值"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2752
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2789
msgid "Quick search"
msgstr "快速搜索"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:118
msgid "Remove filter"
msgstr "移除筛选"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802
msgid "Resume"
msgstr "继续"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2451
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2488
msgid "Row tint"
msgstr "行着色"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2589
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 "勾选后行着色显示放行/拒绝行背景。可选经典(绿/红,默认)或无障碍(青绿/橙)。操作文本在两种模式下均保持着色。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2608
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2645
msgid "Row tint used a local color fallback because the active LuCI theme did not apply pass/deny backgrounds."
msgstr "由于当前 LuCI 主题未应用放行/拒绝背景色,行着色使用了本地颜色回退。"
@@ -554,19 +558,19 @@ msgstr "由于当前 LuCI 主题未应用放行/拒绝背景色,行着色使
msgid "Rule"
msgstr "规则"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2660
msgid "Rule name details"
msgstr "规则名称详情"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:785
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:816
msgid "Rule-name lookup does not change your firewall rules."
msgstr "规则名称查询不会更改您的防火墙规则。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:770
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:801
msgid "Rule-name lookup failed."
msgstr "规则名称查询失败。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:777
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
msgid "Rule-name lookup reached a safety limit."
msgstr "规则名称查询已达到安全限制。"
@@ -574,93 +578,97 @@ msgstr "规则名称查询已达到安全限制。"
msgid "SPort"
msgstr "源端口"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:402
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:409
msgid "Search"
msgstr "搜索"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1433
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1464
msgid "Server adaptive protection is disabled."
msgstr "服务器自适应保护已禁用。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1441
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1472
msgid "Server limited fetch: the router is under load; at most %d log lines per poll."
msgstr "服务器已限制抓取:路由器负载较高;每次轮询最多 %d 行日志。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1446
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1477
msgid "Server limited fetch: this poll returned fewer lines to protect the router."
msgstr "服务器已限制抓取:本次轮询返回的行数较少,以保护路由器。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1431
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1462
msgid "Server protection state is unknown."
msgstr "服务器保护状态未知。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2478
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:151
msgid "Show full message"
msgstr "显示完整消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2515
msgid "Show hostnames"
msgstr "显示主机名"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2449
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2486
msgid "Show pass/deny row background colors"
msgstr "显示放行/拒绝行背景色"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2772
msgid "Show rows"
msgstr "显示行"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1511
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1542
msgid "Showing a compact summary of the top of the shown sample."
msgstr "显示当前样本顶部的简要摘要。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2659
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2696
msgid "Simple"
msgstr "简洁"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:788
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:819
msgid "Some entries may show names derived from their log prefixes. Searching by a friendly rule name may miss those entries."
msgstr "部分条目可能显示由日志前缀生成的名称。按易读的规则名称搜索可能会遗漏这些条目。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:839
msgid "Some rule names may be missing"
msgstr "部分规则名称可能缺失"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:406
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:413
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:45
msgid "Source"
msgstr "源地址"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2802
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2839
msgid "Source IP contains (! to exclude)"
msgstr "源 IP 包含(! 表示排除)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:408
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:415
msgid "Source port"
msgstr "源端口"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2807
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2844
msgid "Source port (! to exclude)"
msgstr "源端口(! 表示排除)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1513
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1544
msgid "Summary data is unavailable; show rows to inspect the sample."
msgstr "摘要数据不可用;显示行以检查样本。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2726
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2763
msgid "Summary mode"
msgstr "摘要模式"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2506
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2543
msgid "Summary mode: when the router responds slowly, a compact summary replaces the table. Use Show rows to see the table; it returns automatically after three fast polls."
msgstr "摘要模式:路由器响应较慢时,会以简要摘要代替表格。点击「显示行」可查看表格;连续三次快速轮询后会自动恢复。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1546
msgid "Summary was bounded before all categories could be included."
msgstr "摘要已受限,无法包含所有类别。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2526
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563
msgid "Switching tabs pauses polling; returning performs one catch-up poll. Hostnames are off by default because lookups add work."
msgstr "切换到其他标签页会暂停轮询;返回时执行一次追赶轮询。主机名默认关闭,因为解析会增加工作量。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1606
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1637
msgid "Table refresh throttled: the event rate is high, so the table repaints less often to protect the browser. The buffer still updates; refresh resumes automatically."
msgstr "表格刷新已限流:事件速率较高,表格重绘频率降低以保护浏览器。缓冲区仍在更新;刷新将自动恢复。"
@@ -668,23 +676,23 @@ msgstr "表格刷新已限流:事件速率较高,表格重绘频率降低以
msgid "The firewall did not reload; saved and live logging may differ."
msgstr "防火墙未能重新加载;已保存的日志设置与当前生效状态可能不一致。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2536
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2573
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 施加。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1709
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1740
msgid "The router could not read the firewall log — retrying…"
msgstr "路由器无法读取防火墙日志 — 正在重试…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2531
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2568
msgid "The router log ring may evict older events before fwlive reads them. fwlive cannot recover evicted entries or change forwarding behavior."
msgstr "路由器日志环形缓冲区可能在 fwlive 读取前淘汰较旧事件。fwlive 无法恢复已淘汰的条目,也不会改变转发行为。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2491
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2528
msgid "The table updates automatically when your firewall logs traffic. Use Pause if it moves too fast."
msgstr "防火墙记录流量时表格会自动更新。如果滚动太快,请使用「暂停」。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2611
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648
msgid "Theme tint fallback"
msgstr "主题着色回退"
@@ -700,15 +708,15 @@ msgstr "此路由器报告了 Live View 尚无法识别的日志阻止项。"
msgid "Time"
msgstr "时间"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1520
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
msgid "Top drops"
msgstr "主要丢弃"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1523
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1554
msgid "Top rules"
msgstr "主要规则"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1548
msgid "Top talkers"
msgstr "主要通信方"
@@ -728,16 +736,16 @@ msgstr "开启 WAN 防火墙区域的日志记录并重新加载防火墙。"
msgid "Undo:"
msgstr "撤销:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2555
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2592
msgid "Use Detail for all columns (flags, length, raw message)."
msgstr "使用「详细」可查看全部列(标志、长度、原始消息)。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2641
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2647
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2678
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684
msgid "View"
msgstr "视图"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:976
msgid "WAN drop/reject logging is off."
msgstr "WAN 丢弃/拒绝日志记录已关闭。"
@@ -745,11 +753,11 @@ msgstr "WAN 丢弃/拒绝日志记录已关闭。"
msgid "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up here. Normal LAN browsing will not."
msgstr "WAN 丢弃/拒绝日志记录已开启。被阻止的 WAN 入站流量会显示在这里;正常的 LAN 浏览不会。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:930
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:961
msgid "WAN drop/reject logging is on. Blocked inbound traffic should appear here as it happens — not normal LAN browsing."
msgstr "WAN 丢弃/拒绝日志记录已开启。被阻止的入站流量会实时显示在这里 — 正常的 LAN 浏览不会。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:932
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:963
msgid "WAN logging is already enabled."
msgstr "WAN 日志记录已处于启用状态。"
@@ -778,25 +786,25 @@ msgstr "WAN 日志不可用:无 WAN 区域"
msgid "Waiting for firewall events"
msgstr "正在等待防火墙事件"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1770
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2636
msgid "Watching"
msgstr "监视中"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2701
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2738
msgid "Wrap"
msgstr "自动换行"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:72
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2757
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794
msgid "block"
msgstr "拦截"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1255
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1286
msgid "buffer filling"
msgstr "缓冲区填充中"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1259
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1290
msgid "buffer full"
msgstr "缓冲区已满"
@@ -808,7 +816,7 @@ msgstr "包含"
msgid "default 10/minute"
msgstr "默认 10 条/分钟"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1264
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1295
msgid "display limited to %d rows"
msgstr "显示上限为 %d 行"
@@ -816,12 +824,12 @@ msgstr "显示上限为 %d 行"
msgid "does not contain"
msgstr "不包含"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2758
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:75
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2795
msgid "drop"
msgstr "丢弃"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1268
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1299
msgid "hostname lookup paused"
msgstr "主机名解析已暂停"
@@ -829,16 +837,16 @@ msgstr "主机名解析已暂停"
msgid "is"
msgstr "是"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1254
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1285
msgid "loading buffer"
msgstr "正在加载缓冲区"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1695
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1726
msgid "loading…"
msgstr "加载中…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1493
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1524
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1533
msgid "none"
msgstr "无"
@@ -846,101 +854,104 @@ msgstr "无"
msgid "not"
msgstr "非"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1921
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1952
msgid "not AH"
msgstr "非 AH"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1920
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1951
msgid "not ESP"
msgstr "非 ESP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1919
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1950
msgid "not GRE"
msgstr "非 GRE"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1916
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1947
msgid "not ICMP"
msgstr "非 ICMP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1917
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1948
msgid "not ICMPv6"
msgstr "非 ICMPv6"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1918
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1949
msgid "not IGMP"
msgstr "非 IGMP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1922
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1953
msgid "not SCTP"
msgstr "非 SCTP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1914
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1945
msgid "not TCP"
msgstr "非 TCP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1915
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1946
msgid "not UDP"
msgstr "非 UDP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2763
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2800
msgid "not block"
msgstr "非阻止"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2762
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2799
msgid "not drop"
msgstr "非丢弃"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2761
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2798
msgid "not pass"
msgstr "非放行"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2764
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2801
msgid "not reject"
msgstr "非拒绝"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2765
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2802
msgid "not unknown"
msgstr "非未知"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2780
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2817
msgid "or type…"
msgstr "或输入…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2756
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2793
msgid "pass"
msgstr "通过"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1266
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1297
msgid "polling slowed"
msgstr "轮询已放慢"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2759
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:76
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2796
msgid "reject"
msgstr "拒绝"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1270
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1301
msgid "scroll frozen — scroll to top to follow live"
msgstr "滚动已冻结 — 滚动到顶部以恢复跟随"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1267
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1298
msgid "server limited fetch"
msgstr "服务器已限制抓取"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1261
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1292
msgid "table refresh throttled"
msgstr "表格刷新已限流"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:63
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:65
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2797
msgid "unknown"
msgstr "未知"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:750
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:781
msgid "using fw4"
msgstr "使用 fw4"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:176
msgid "· %s"
msgstr "· %s"
#~ msgid "Click a row (Time or other non-link cells) to see the full log line (Simple view)."
#~ msgstr "点击一行(时间或其他非链接单元格)可查看完整日志行(简单视图)。"
@@ -4,7 +4,7 @@
#
# Layer 1 adaptive poll cap. Sourced by rpcd/fwlive.
# Always on unless test/triage override (no UCI / no product config):
# FWLIVE_ADAPTIVE=0|false|off|no
# FWLIVE_ADAPTIVE=0|false|off|no (case-insensitive words)
# or sentinel ${FWLIVE_ADAPTIVE_OFF_FILE:-<state-dir>/fwlive-adaptive-off}
# (default under /var/run next to state — not world-writable /tmp).
#
@@ -64,7 +64,7 @@ fwlive_adaptive_off_path() {
fwlive_adaptive_enabled() {
case "${FWLIVE_ADAPTIVE:-1}" in
0|false|off|no|FALSE|OFF|NO) return 1 ;;
0|[fF][aA][lL][sS][eE]|[oO][fF][fF]|[nN][oO]) return 1 ;;
esac
[ -e "$(fwlive_adaptive_off_path)" ] && return 1
return 0
@@ -158,7 +158,7 @@ function json_escape(s, out, i, c) {
else if (c == "\n") out = out "\\n"
else if (c == "\r") out = out "\\r"
else if (c == "\t") out = out "\\t"
else if (c < " ") out = out " "
else if (c in json_control_ord) out = out sprintf("\\u%04x", json_control_ord[c])
else out = out c
}
return out
@@ -199,6 +199,8 @@ function summary_json(out) {
return out
}
BEGIN {
for (i = 1; i < 32; i++) json_control_ord[sprintf("%c", i)] = i
json_control_ord[sprintf("%c", 127)] = 127
if (MODE == "json_reply") {
ORS = ""
printf "{\"log\":["
+146 -115
View File
@@ -46,7 +46,7 @@ slug_key() {
RULES_MAP_MAX_KEYS=512
RULES_MAP_MAX_BYTES=65536
# First-wins dups never increment _map_keys, so they would otherwise walk
# the whole dump. Bound map_prefix_with_label calls independently.
# the whole dump. Bound prepared prefix records independently.
RULES_MAP_MAX_LINES=512
# Append one already-escaped "key":"value" to OUT. Value may be empty.
@@ -74,16 +74,19 @@ map_add() {
;;
esac
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).
# Callers apply UCI, then labeled (!fw4:) prefixes, then unlabeled
# cosmetics so first-wins prefers authoritative names.
esc_val=$(printf '%s' "$val" | json_escape)
map_add_escaped "$esc_key" "$esc_val"
}
# Prepared nft records contain JSON string content with no raw tab/newline.
# Keep one global first-wins map and identical key/byte caps for both sources.
map_add_escaped() {
esc_key="$1"
esc_val="$2"
[ -n "$esc_key" ] && [ -n "$esc_val" ] || return 0
case "$OUT" in
*"\"$esc_key\":"*) return 0 ;;
esac
esc_val=$(printf '%s' "$val" | json_escape)
_pair_len=$(( ${#esc_key} + ${#esc_val} + 5 ))
_next_len=$(( ${#OUT} + _pair_len ))
[ -n "$OUT" ] && _next_len=$((_next_len + 1))
@@ -150,10 +153,6 @@ is_uci_style_name() {
return 0
}
normalize_log_prefix() {
printf '%s' "$1" | sed 's/[[:space:]:]*$//'
}
read_rpc_input() {
if [ -n "$1" ]; then
printf '%s' "$1"
@@ -162,31 +161,6 @@ read_rpc_input() {
fi
}
map_prefix_with_label() {
prefix="$1"
label="$2"
if [ "${_map_lines:-0}" -ge "$RULES_MAP_MAX_LINES" ]; then
_map_overflow=1
return 0
fi
_map_lines=$(( ${_map_lines:-0} + 1 ))
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=$(printf '%s\n' "$prefix" | tr '-' ' ')
map_add "$prefix" "$cosmetic"
if [ "$slug" != "$prefix" ]; then
map_add "$slug" "$cosmetic"
fi
fi
}
map_uci_rule_names() {
# Line-wise so each name stays one token. Names outside the key charset
# are skipped; one shared line cap also bounds duplicate UCI names that
@@ -228,7 +202,7 @@ EOF
# Raw tabs become \t and backslashes become \\ so the separator tab is
# unique. Walk bytes: BusyBox awk ≥1.37 gsub cannot emit those sequences.
nft_dump_fields() {
LC_ALL=C awk '
LC_ALL=C awk -v mode="${1:-raw}" -v tag="$FW4_TAG" -v max="$RULES_MAP_MAX_LINES" '
function last_at(s, needle, p, i, found) {
found = 0
p = 1
@@ -254,6 +228,60 @@ nft_dump_fields() {
}
return out
}
function json_string(s, out, i, c) {
out = ""
for (i = 1; i <= length(s); i++) {
c = substr(s, i, 1)
if (c == "\\") out = out "\\\\"
else if (c == "\"") out = out "\\\""
else if (c == "\t") out = out "\\t"
else if (c == "\r") out = out "\\r"
else if (c == "\n") out = out "\\n"
else if (c in controls) out = out sprintf("\\u%04x", controls[c])
else out = out c
}
return out
}
function slug(s, out, i, c) {
out = ""
for (i = 1; i <= length(s); i++) {
c = substr(s, i, 1)
out = out ((c == " " || c == "_") ? "-" : tolower(c))
}
return out
}
function cosmetic(s, out, i, c) {
out = ""
for (i = 1; i <= length(s); i++) {
c = substr(s, i, 1)
out = out (c == "-" ? " " : c)
}
return out
}
function prepared(prefix, comment, label, kind, alias, value) {
label = ""
if (index(comment, tag) == 1) {
label = substr(comment, length(tag) + 1)
if (label !~ /^[A-Za-z0-9_-]+$/) label = ""
}
if (prefix == "") {
if (label == "") return
prefix = label
}
kind = label == "" ? "unlabeled" : "labeled"
# Retain one excess record to signal the shell cap, but stop doing
# normalization/encoding for further records of either priority.
if (++prepared_count[kind] > max + 1) return
sub(/[[:space:]:]*$/, "", prefix)
alias = slug(prefix)
if (alias == prefix) alias = ""
value = label == "" ? cosmetic(prefix) : label
print kind "\t" json_string(prefix) "\t" json_string(alias) "\t" json_string(value)
}
BEGIN {
for (i = 1; i < 32; i++) controls[sprintf("%c", i)] = i
controls[sprintf("%c", 127)] = 127
}
function quoted_at(s, needle, i, rest, out, c, n, esc, j) {
i = last_at(s, needle)
if (i == 0)
@@ -285,6 +313,10 @@ nft_dump_fields() {
comment = quoted_at($0, "comment \"")
if (prefix == "" && comment == "")
next
if (mode == "prepared") {
prepared(prefix, comment)
next
}
prefix = tsv_escape(prefix)
comment = tsv_escape(comment)
print prefix "\t" comment
@@ -329,49 +361,34 @@ _nft_tsv_unescape() {
_tsv_out="$_out"
}
# pass=labeled|unlabeled|all — labeled before unlabeled so !fw4: beats
# an earlier cosmetic for the same prefix under first-wins.
# Reads TSV from nft_dump_fields; stops once the map cap trips.
# Labeled before unlabeled preserves authoritative fw4 names over cosmetics.
# Each prepared record is kind<TAB>escaped-key<TAB>escaped-alias<TAB>escaped-value.
# JSON escapes keep raw separator tabs out of fields. All per-prefix work is
# done by the one awk dump stage; this loop uses shell builtins only.
map_from_nft_stream() {
pass="${1:-all}"
_tab=$(printf '\t')
_esc_t=$(printf '\\t')
_esc_b=$(printf '\\\\')
# IFS= keeps a leading tab (empty prefix + comment-only !fw4: line).
while IFS= read -r line || [ -n "$line" ]; do
[ -n "$_map_overflow" ] && break
[ -n "$line" ] || continue
prefix="${line%%${_tab}*}"
case "$line" in
*"$_tab"*) comment="${line#*"$_tab"}" ;;
*) comment="" ;;
kind="${line%%${_tab}*}"
case "$pass" in
labeled|unlabeled) [ "$kind" = "$pass" ] || continue ;;
esac
_nft_tsv_unescape "$prefix"
prefix="$_tsv_out"
_nft_tsv_unescape "$comment"
comment="$_tsv_out"
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
case "$pass" in
labeled) [ -n "$label" ] || continue ;;
unlabeled) [ -z "$label" ] || continue ;;
esac
map_prefix_with_label "$prefix" "$label"
elif [ -n "$label" ]; then
case "$pass" in
unlabeled) continue ;;
esac
map_prefix_with_label "$label" "$label"
if [ "${_map_lines:-0}" -ge "$RULES_MAP_MAX_LINES" ]; then
_map_overflow=1
break
fi
_map_lines=$(( ${_map_lines:-0} + 1 ))
_rest="${line#*"$_tab"}"
_key="${_rest%%${_tab}*}"
_rest="${_rest#*"$_tab"}"
_alias="${_rest%%${_tab}*}"
_value="${_rest#*"$_tab"}"
map_add_escaped "$_key" "$_value"
[ -n "$_map_overflow" ] && break
if [ -n "$_alias" ]; then
map_add_escaped "$_alias" "$_value"
fi
done
}
@@ -425,6 +442,9 @@ _fwlive_tmp_cleanup() {
rm -f "$_nft_tsv"
_nft_tsv=
fi
# Closing these reserved descriptors releases anonymous dump/TSV files
# when no surviving helper still holds them. SIGKILL cannot run this trap.
command exec 7>&- 8>&-
}
# Same five traps as fwlive-log-filter.sh. The nft dump waits in the
# background so a trapped SIGTERM is not deferred behind a foreground helper.
@@ -436,47 +456,53 @@ trap '_fwlive_tmp_cleanup; exit 143' 15
enrich_rules_dump() {
_dump_prefix="$1"
_rules_dump=$(_fwlive_mktemp "$_dump_prefix") || _rules_dump=''
if [ -n "$_rules_dump" ]; then
# One dump serves detect + parse. Empty/failed dump is not nft
# (unknown/no_backend, same as a failed detect probe).
# Background + wait: dash defers trapped SIGTERM until a
# foreground child exits; wait is interruptible.
# Clear inherited traps so the child's EXIT cannot rm the dump.
( trap - 0 1 2 3 15; nft_list_ruleset >"$_rules_dump" 2>/dev/null ) &
_nft_wait=$!
if wait "$_nft_wait"; then
_nft_wait=
backend=nft
# Stream awk output to a second temp so a huge dump is
# not materialized in the rpcd worker's shell heap.
_nft_tsv=$(_fwlive_mktemp "fwlive-nft-tsv")
if [ -z "$_nft_tsv" ]; then
_enrich_error='mktemp_failed'
else
# awk under set -eu must not abort the plugin before JSON.
if nft_dump_fields <"$_rules_dump" >"$_nft_tsv"; then
map_from_nft_stream labeled <"$_nft_tsv"
if [ -z "$_map_overflow" ]; then
map_from_nft_stream unlabeled <"$_nft_tsv"
fi
else
_enrich_error='tsv_failed'
fi
rm -f "$_nft_tsv"
_nft_tsv=
fi
else
_nft_wait=
fi
rm -f "$_rules_dump"
_rules_dump=
else
# nft is on PATH (caller checked); skip enrichment without a dump.
# Open then unlink before starting long-running work. command removes
# exec special-builtin fatal-exit behavior if the open fails under dash.
if [ -z "$_rules_dump" ] || ! command exec 7<>"$_rules_dump"; then
_enrich_error='mktemp_failed'
backend=nft
_fwlive_tmp_cleanup
return 0
fi
if ! rm -f "$_rules_dump"; then
_enrich_error='mktemp_failed'
backend=nft
_fwlive_tmp_cleanup
return 0
fi
_rules_dump=''
# SIGKILL after unlink cannot leave a directory entry, but an orphan can
# retain this tmpfs inode through its fd. The setup window before unlink
# and accepted direct-helper lifetime/resource limits are unchanged.
( trap - 0 1 2 3 15; nft_list_ruleset >&7 2>/dev/null ) &
_nft_wait=$!
if wait "$_nft_wait"; then
_nft_wait=
backend=nft
_nft_tsv=$(_fwlive_mktemp "fwlive-nft-tsv") || _nft_tsv=''
if [ -z "$_nft_tsv" ] || ! command exec 8<>"$_nft_tsv"; then
_enrich_error='mktemp_failed'
elif ! rm -f "$_nft_tsv"; then
_enrich_error='mktemp_failed'
else
_nft_tsv=''
# Reopen via procfs for a fresh file offset. A direct fd dup would
# share the writer offset at EOF, including on the second TSV pass.
if nft_dump_fields prepared </proc/self/fd/7 >&8; then
map_from_nft_stream labeled </proc/self/fd/8
if [ -z "$_map_overflow" ]; then
map_from_nft_stream unlabeled </proc/self/fd/8
fi
else
_enrich_error='tsv_failed'
fi
fi
else
_nft_wait=
fi
_fwlive_tmp_cleanup
}
build_rules_map() {
@@ -664,8 +690,10 @@ fetch_firewall_logs() {
*) _filter_ok=0 ;;
esac
end_cs=$(fwlive_adaptive_clock_cs)
# Uptime wrap / skew: end < start — do not record as cold.
if [ "$_filter_ok" = 1 ] && [ "$end_cs" -ge "$start_cs" ]; then
# Missing uptime (zero) or wrap/skew is not a healthy cold sample.
# Equal nonzero samples remain a valid sub-centisecond duration.
if [ "$_filter_ok" = 1 ] && [ "$start_cs" -gt 0 ] && \
[ "$end_cs" -gt 0 ] && [ "$end_cs" -ge "$start_cs" ]; then
_dur=$(((end_cs - start_cs) * 10))
fwlive_adaptive_record "$_dur" "$lines"
fi
@@ -1128,7 +1156,7 @@ run_selftest() {
return 1
fi
# Tabbed prefix plus !fw4: label: map key is the full prefix.
got=$(printf '%s\n' "log prefix \"fwlive${_tab}tabbed\" comment \"!fw4: TabRule\"" | nft_dump_fields)
got=$(printf '%s\n' "log prefix \"fwlive${_tab}tabbed\" comment \"!fw4: TabRule\"" | nft_dump_fields prepared)
OUT=''
_map_keys=0
_map_lines=0
@@ -1146,6 +1174,7 @@ EOF
echo "comment-only TSV: expected leading tab + !fw4: GoodName got '$got'" >&2
return 1
fi
got=$(printf '%s\n' 'comment "!fw4: GoodName"' | nft_dump_fields prepared)
OUT=''
_map_keys=0
_map_lines=0
@@ -1268,7 +1297,9 @@ Address: 127.0.0.1:53")
# jshn failures must log a named cause; stdout stays the numeric default.
# logger() override is inherited by the $() subshell; command -v still finds
# the binary, so the unqualified logger call is what this intercepts.
_poll_log="/tmp/fwlive-poll-cause.$$"
# Root-run selftest uses the same checked sticky /tmp as rules dumps;
# caller-controlled TMPDIR must not choose an unsafe scratch directory.
_poll_log=$(_fwlive_mktemp "fwlive-poll-cause") || return 1
: > "$_poll_log"
logger() { printf '%s\n' "$*" >> "$_poll_log"; }
+2 -1
View File
@@ -13,11 +13,12 @@ LUCI_DEPENDS:= \
+kmod-nft-tproxy \
+ip-full \
+kmod-tun \
+uclient-fetch \
+ucode-mod-digest
PKG_NAME:=luci-app-homeproxy-pro
PKG_VERSION:=28.10.1.14
PKG_RELEASE:=16
PKG_RELEASE:=17
LUCI_BASENAME:=homeproxy-pro
@@ -34,6 +34,30 @@ const HTTP_PING_KEEPALIVE_HINT = _('The timeout (in seconds) that after performi
TLS listener failed with nothing pointing at the path. */
const HP_CERT_PATH_ROOTS = [ '/etc/homeproxy-pro/certs/', '/etc/acme/', '/etc/ssl/' ];
/* Rule-set source files: a `type: local` rule-set's `path` and a `type: remote`
one's `initial_path`. This list mirrors RULE_PATH_ROOTS in
/etc/homeproxy-pro/scripts/homeproxy-pro.uc, and tests/arch-guard.sh guard 50 fails
when the two drift apart - the same lock guard 29 puts on the certificate
list, for the same reason: the UI's check is only UX, UCI can be set from
anywhere on the LAN, and a path the UI accepted but the backend dropped made
the field vanish from the generated configuration with nothing pointing at
the path.
It is deliberately a single root, and deliberately narrower than the
general HP_DIR-shaped gate the backend also has: a rule-set belongs in the
archive, which the package creates at install time
(/etc/uci-defaults/luci-homeproxy-pro) and before every generation
(runtime/service.sh's hp_prepare_ruleset_dir). */
const HP_RULE_PATH_ROOTS = [ '/etc/homeproxy-pro/ruleset/' ];
/* The archive path offered as the datalist entry on the rule-set path fields,
so the default the backend expects is one click away instead of something
the user has to know. Kept as its own constant so the placeholder, the
datalist and the validator's message all quote the same directory - a
placeholder that named a path the validator then refused would be its own
small version of the bug this list exists to prevent. */
const HP_RULE_PATH_DEFAULT = '/etc/homeproxy-pro/ruleset/example.srs';
/* Methods whose failure has already been reported, so that a polled call
cannot repeat the same notification every few seconds. */
const rpc_warned = new Set();
@@ -58,6 +82,80 @@ for (let i = 0; i < 64; i++)
MD5_K[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 4294967296);
return baseclass.extend({
/* The archive path the rule-set form offers as its datalist entry and
placeholder. Exported rather than left module-local so the view quotes
this one string instead of repeating the path: a placeholder naming a
directory the validator then refuses is its own small version of the
drift guard 50 exists to catch, and the placeholder is exactly what a
user copies. */
rule_path_default: HP_RULE_PATH_DEFAULT,
/* Parse sing-box's rule-set download failures out of a log.
*
* With `ruleset_safe_start` on, a remote rule-set that cannot be
* downloaded is no longer a failed start: sing-box reads the `initial_path`
* fallback, comes up, and retries in the background, logging
*
* 2026-10-03 01:15:42 ERROR router: fetch rule-set geoip-cn: Get "https://…": dial tcp …
*
* That is a good trade, and it is also invisible: the instance is healthy,
* the health gate passes, the LAN is proxied - and the rule-set that is
* supposed to keep mainland traffic direct matches NOTHING, so all of it
* goes to `final`. The status page is the only place a user looks without
* ssh, so it has to show this.
*
* Two deliberate choices about the format:
*
* - the tag and the reason are matched on the stable part
* (`fetch rule-set <tag>:`) and nothing assumes sing-box's timestamp
* or level layout. A rule-set whose download failed long ago and whose
* log line moves out of the file must not read as "no failures", so a
* missing timestamp is omitted rather than faked.
* - the last occurrence of a tag wins. The file is truncated at 50 KB by
* clean_log.sh, so a tag that failed every 24 hours is in there
* repeatedly, and the most recent reason is the one that is true now.
*
* The timestamp shape is the one thing that IS pinned, and it was pinned
* from a real 1.14.2 line rather than guessed - the device writes
*
* +0800 2026-10-03 09:29:34 ERROR router: fetch rule-set geoip-cn: Get ...
*
* i.e. a UTC offset BEFORE the date. An earlier version of this anchored
* the date at the start of the line and therefore returned no time for
* every real line, which is the one shape the panel is supposed to show.
* The offset is optional in the pattern so a build that omits it still
* parses.
*
* Returns [{ tag, reason, at }], newest last. Never throws: this reads a
* file that may be empty, half-written, or rotated mid-read. */
parseRuleSetFetchFailures(text) {
const out = [];
if (!text)
return out;
const seen = {};
for (const line of String(text).split('\n')) {
const m = /fetch rule-set\s+([^\s:]+):\s*(.*)$/.exec(line);
if (!m)
continue;
const tag = m[1];
const at = /^(?:[+-]\d{4}\s+)?(\d{4}-\d{2}-\d{2}[ T][0-9:.]+)/.exec(line);
if (seen[tag] !== undefined) {
out[seen[tag]].reason = m[2].trim();
out[seen[tag]].at = at ? at[1] : null;
continue;
}
seen[tag] = out.length;
out.push({ tag: tag, reason: m[2].trim(), at: at ? at[1] : null });
}
return out;
},
dns_strategy: {
'': _('Default'),
'prefer_ipv4': _('Prefer IPv4'),
@@ -1108,6 +1206,23 @@ return baseclass.extend({
return true;
},
validateRuleSetPath(section_id, value) {
if (section_id && value)
/* HP_RULE_PATH_ROOTS is the same list the backend enforces
(RULE_PATH_ROOTS, guard 50). The `..` rejection is explicit for
the same reason as in validateCertificatePath: a prefix match
alone accepts /etc/homeproxy-pro/ruleset/../../etc/shadow, and
sing-box opens these files as root. The backend is the authority
- this only saves a save that the generator would refuse a
moment later with a message the user reads in a log file rather
than on the page they are editing. */
if (value.match(/(^|\/)\.\.(\/|$)/) ||
!HP_RULE_PATH_ROOTS.some((root) => value.indexOf(root) === 0 && value.length > root.length))
return _('Expecting: %s').format(_('/etc/homeproxy-pro/ruleset/...'));
return true;
},
validatePortRange(section_id, value) {
if (section_id && value) {
value = value.match(/^(\d+)?\:(\d+)?$/);
@@ -52,15 +52,38 @@ function render(ctx) {
so.default = 'remote';
so.rmempty = false;
so = ss.option(form.ListValue, 'format', _('Format'));
so = ss.option(form.ListValue, 'format', _('Format'),
_('Rule-set file format. Leave it empty and HomeProxy reads it from the file itself; a value that does not match the file is corrected and reported in the log.'));
so.value('binary', _('Binary file'));
so.value('source', _('Source file'));
so.default = 'binary';
so.rmempty = false;
/* rmempty, and no default.
*
* A default of 'binary' plus rmempty=false meant EVERY local rule-set
* carried an explicit format - and an explicit value beats sing-box's
* extension inference, so "add a local rule-set, pick my example.json,
* forget to change Format" produced a configuration that parsed JSON as
* a compiled .srs and was rejected at apply time.
*
* Leaving the field empty is now the recommended state rather than an
* accident: the generator reads the first bytes of the file and declares
* the format itself (generator/ruleset.uc's resolveFormat), correcting a
* wrong value and saying so in the log. Writing a default here would
* put a value back on every rule-set for that code to undo. */
so.rmempty = true;
so = ss.option(form.Value, 'path', _('Path'));
so = ss.option(form.Value, 'path', _('Path'),
_('Rule-set file in %s. Copy it there first: the generator refuses a rule-set whose file is missing.').format('/etc/homeproxy-pro/ruleset/'));
so.datatype = 'file';
so.placeholder = '/etc/homeproxy-pro/ruleset/example.json';
/* The archive is the only directory the backend admits (RULE_PATH_ROOTS),
* and it is created for the user - at install time and again before every
* generation. Offering it as the datalist entry is what turns "point at a
* file under a directory that may not exist" into a one-click default.
* The same field used to carry a placeholder naming example.json while
* the UI defaulted the format to binary, so the file it suggested was one
* the generated configuration would then refuse to parse. */
so.value(hp.rule_path_default);
so.placeholder = hp.rule_path_default;
so.validate = hp.validateRuleSetPath;
so.rmempty = false;
so.depends('type', 'local');
so.modalonly = true;
@@ -108,8 +131,11 @@ function render(ctx) {
so.depends('type', 'remote');
so = ss.option(form.Value, 'initial_path', _('Initial path'),
_('Local file with initial rule-set content; avoids blocking startup on first download (1.14).'));
_('Local file with initial rule-set content; avoids blocking startup on first download (1.14). Must be under %s.').format('/etc/homeproxy-pro/ruleset/'));
so.datatype = 'file';
so.value(hp.rule_path_default);
so.placeholder = hp.rule_path_default;
so.validate = hp.validateRuleSetPath;
so.depends('type', 'remote');
so.modalonly = true;
@@ -130,8 +156,37 @@ function render(ctx) {
so.modalonly = true;
so = ss.option(form.Value, 'update_interval', _('Update interval'),
_('Update interval of rule set.'));
so.placeholder = '1d';
_('Update interval of rule set, e.g. 24h or 3600 (seconds).'));
/* 24h, not 1d.
*
* sing-box parses update_interval as a Go duration, and Go's units are
* ns/us/ms/s/m/h - there is no day. The placeholder used to read "1d",
* which is the one value a user is most likely to copy verbatim into a
* field that then rejects the whole configuration. 24h is the same
* interval, is a unit Go reads, and is exactly what this generator emits
* for its own built-in rule-sets (generator/route.uc).
*
* A bare number is accepted because the generator normalises it: the
* value is passed through strToTime(), which appends "s" to anything
* that does not already end in a unit - that is the fix for the measured
* `time: missing unit in duration "3600"` failure. */
so.placeholder = '24h';
so.validate = function(section_id, value) {
/* Empty means "no update_interval", which sing-box reads as its own
* default - the field is optional, so it must not be made required. */
if (!section_id || !value)
return true;
/* What Go's time.ParseDuration accepts: a sequence of
* number+unit groups (so 1h30m is fine), or a bare number, which
* strToTime() will turn into seconds. Everything else - "1d", "1w",
* "daily", "24 h" - is refused here, on the page, instead of becoming
* a rejected configuration the user only learns about from a log. */
if (/^\d+$/.test(value) || /^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/.test(value))
return true;
return _('Expecting: %s').format(_('update interval like 24h, 1h30m or 3600'));
};
so.depends('type', 'remote');
/* Rule set settings end */
}
@@ -75,6 +75,77 @@ const ensureLogPoll = hp.statusPoller({
}
});
/* --- rule-set download failures ------------------------------------------
*
* With ruleset_safe_start on, a remote rule-set that cannot be downloaded no
* longer stops the service: sing-box reads the empty initial_path fallback,
* binds, and retries in the background. The instance is healthy and the LAN is
* proxied - and the rule-set that keeps mainland traffic direct matches
* nothing, so all of it goes to `final`.
*
* That is the trade, and it is the reason this block exists: the degradation
* has no other symptom. The health gate does not look at rule-sets at all
* (there is nothing in health.sh that could), so the service reports itself
* healthy while a rule-set is silently empty.
*
* sing-box-c.log is read rather than a new RPC: the ACL already grants the
* browser read on that file for the log viewer below, so this costs no
* permission and no new method to keep in step with the method table.
*
* Whether a fallback is actually in play is read from UCI rather than guessed:
* with the opt-in OFF, a failing rule-set means the instance would not have
* started at all, which is a different and much more visible problem - so the
* wording has to differ, not just the colour. */
const ruleSetNode = E('div', { 'id': 'ruleset_status' },
E('img', {
'src': L.resource('icons/loading.svg'),
'alt': _('Loading'),
'style': 'vertical-align:middle'
}, _('Collecting data...'))
);
function renderRuleSetStatus() {
/* Same poller as the log views, so there is still one handler per view
* no matter how often render() runs. */
ensureLogPoll();
fs.read_direct(`${hp_dir}/sing-box-c.log`, 'text')
.then((text) => {
const failures = hp.parseRuleSetFetchFailures(text);
const safeStart = (uci.get('homeproxy-pro', 'config', 'ruleset_safe_start') || '0') === '1';
if (!failures.length) {
dom.content(ruleSetNode, E('span', { 'style': 'color:green' },
[_('No rule-set download failures in the current log.')]));
return;
}
dom.content(ruleSetNode, E('div', [
E('p', { 'style': 'color:red' }, [
_('%d rule-set(s) cannot be downloaded.').format(failures.length)
]),
E('p', { 'style': 'color:gray' }, [
safeStart
? _('Startup fallback in use: these rule-sets are EMPTY, so the traffic they would have split falls through to the default route. The service is up, but the split is not in effect until the download succeeds.')
: _('Startup fallback is OFF, so a rule-set that cannot be downloaded prevents the service from starting at all. The service is running, so the log below is from an earlier attempt or the download has since recovered.')
]),
E('ul', {}, failures.map((f) => E('li', [
E('code', [ f.tag ]),
' — ',
/* Text nodes, never markup: the reason is a string
* sing-box wrote, and it contains a URL. */
f.reason || _('(no reason recorded)'),
f.at ? E('small', { 'style': 'color:gray' }, [ ' — ' + f.at ]) : ''
])))
]));
})
.catch((err) => {
dom.content(ruleSetNode, E('span', { 'style': 'color:gray' }, [
_('Cannot read the sing-box client log (%s).').format(String(err))
]));
});
}
function getConnStat(o, site) {
o.default = E('div', { 'style': 'cbi-value-field' }, [
E('button', {
@@ -301,6 +372,20 @@ return view.extend({
return node;
}
s = m.section(form.NamedSection, 'config', 'homeproxy-pro', _('Rule sets'));
s.anonymous = true;
/* A dedicated section rather than a fourth log view: the question is
* "is any rule-set currently broken", and the answer has to be a
* verdict plus a per-tag reason, not a wall of log the user has to read
* a timestamp out of. */
o = s.option(form.DummyValue, '_ruleset_status');
o.rawhtml = true;
o.render = function() {
renderRuleSetStatus();
return ruleSetNode;
};
s = m.section(form.NamedSection, 'config', 'homeproxy-pro');
s.anonymous = true;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -42,6 +42,20 @@ config homeproxy-pro 'config'
option health_probe_name 'example.com'
option cn_ip_fallback '1'
option sniffer_advanced_mode '0'
# Start the client even when a remote rule-set cannot be downloaded.
#
# Off by default, and the default has to stay off: the mechanism points a
# remote rule-set at an EMPTY rule-set so that sing-box starts immediately
# instead of fetching during initialization, before the inbounds bind. An
# empty rule-set matches nothing, so everything it would have split falls
# through to `final` - that is a routing change, not just a robustness
# tweak, and on bypass_mainland_china it means mainland destinations can be
# proxied until the download succeeds.
#
# With it off, a cold cache on a fresh install waits for
# raw.githubusercontent.com (through the selected node) before the instance
# will listen at all, and a failure there fails the whole start.
option ruleset_safe_start '0'
option tun_dns_mode ''
option udp_mapping ''
option udp_filtering ''
@@ -0,0 +1 @@
{"version":3,"rules":[]}
@@ -393,6 +393,18 @@ export const Loader = {
udp_nat_max: opt(uci, 'main', 'udp_nat_max'),
cn_ip_fallback: opt(uci, 'main', 'cn_ip_fallback'),
sniffer_advanced_mode: opt(uci, 'main', 'sniffer_advanced_mode'),
/* The "do not block startup on the first rule-set download"
* opt-in. Listed here because this object is an EXPLICIT key
* list: a field the generators read off dm.general that is not
* in it does not read as "absent", it reads as "never set" - and
* with a `|| '0'` fallback in context.uc that is indistinguishable
* from a user who left it off. The first version of the feature
* added the option to /etc/config/homeproxy-pro and read it in
* context.uc, and nothing anywhere said the key was missing: the
* configuration generated cleanly, `sing-box check` passed, and the
* feature simply never turned on. Guard 53 now compares the two
* lists so that class cannot come back quietly. */
ruleset_safe_start: opt(uci, 'main', 'ruleset_safe_start'),
main_urltest_nodes: opt(uci, 'main', 'main_urltest_nodes') || [],
main_urltest_interval: opt(uci, 'main', 'main_urltest_interval'),
main_urltest_tolerance: opt(uci, 'main', 'main_urltest_tolerance'),
@@ -29,7 +29,8 @@ import { lstat, mkdtemp, readfile, writefile } from 'fs';
import { Loader } from './config/loader.uc';
import { generate } from './generator/client.uc';
import { removeBlankAttrs, HP_DIR, RUN_DIR, shellQuote, UCICONFIG_DIR } from './homeproxy-pro.uc';
import { BUILTIN_REMOTE_RULE_SETS, declaresBuiltinRemoteRuleSets, rule_set_tags } from './generator/common.uc';
import { removeBlankAttrs, isEmpty, probeRuleSetFile, ruleSetFormatExtension, ruleSetFormatFromPath, ruleSetInitialFallback, RULESET_EMPTY_BINARY, RULESET_EMPTY_SOURCE, RULESET_INITIAL_DIR, shellQuote, validateRuleSetPath, HP_DIR, RUN_DIR, UCICONFIG_DIR } from './homeproxy-pro.uc';
/* Resolve the GenerationContext inputs. This is the only impure step on the
* client generation path, and it is deliberately here rather than under
@@ -45,10 +46,74 @@ import { removeBlankAttrs, HP_DIR, RUN_DIR, shellQuote, UCICONFIG_DIR } from './
*
* The extra parentheses around the ubus call keep the ?. chain guarded when
* connect() returns null. */
/* Write one EMPTY rule-set to <path>, in <format>.
*
* The fallback exists so that a remote rule-set with no initial file of the
* user's own does not have to be downloaded before the inbounds bind - and on
* a cold cache that is the difference between a router that comes up and one
* that waits on raw.githubusercontent.com (through the node) before it will
* listen at all.
*
* binary is produced by compiling, not by writing bytes: a file produced by
* the sing-box that is running cannot drift from it, and the compiler is
* always there because the very next thing this script does is run it. The
* shipped 14-byte constant is the fallback for the fallback - a router where
* the compile cannot run - and it is a file in the package rather than a byte
* string in the source, so `hexdump -C` on a device can confirm it.
*
* Returns true when <path> holds a usable file afterwards. A false return is
* not fatal: the caller skips the initial_path, and the rule-set then behaves
* exactly as it does today (fetched during initialization, blocking on
* failure). That is the point of the whole opt-in - it must never make a
* situation worse than not having it. */
function writeEmptyRuleSet(path, format) {
const ext = ruleSetFormatExtension(format);
if (!ext)
return false;
system('mkdir -p ' + shellQuote(RULESET_INITIAL_DIR));
if (format === 'source') {
/* No compile needed, and no temp file: the source form is the JSON
* itself. The shipped empty.source.json is the source of record, so
* a missing one means the install is incomplete and the copy is the
* thing that says so. */
if (system('cp -f ' + shellQuote(RULESET_EMPTY_SOURCE) + ' ' + shellQuote(path)) !== 0)
return false;
} else {
/* Compile into the destination directly. A partial output is
* possible if the compile dies, so the file is only accepted when the
* compile succeeded AND something is there - sing-box opening a
* truncated .srs would block startup, which is the outcome this is
* all meant to prevent.
*
* `-o` rather than `--output`, because that is the form
* tests/ucode/test_generators.sh already exercises against this
* binary, and a flag spelling that only ever runs in production is a
* flag spelling nobody has ever seen work. */
if (system('sing-box rule-set compile ' + shellQuote(RULESET_EMPTY_SOURCE)
+ ' -o ' + shellQuote(path) + ' >/dev/null 2>&1') !== 0) {
if (system('cp -f ' + shellQuote(RULESET_EMPTY_BINARY) + ' ' + shellQuote(path)) !== 0)
return false;
}
const st = lstat(path);
if (!st || st.type !== 'file' || st.size <= 0)
return false;
}
/* The jailed client reads this as the sing-box user. writefile() has no
* mode argument and cp preserves the source's, so the mode is set
* explicitly rather than inherited from whatever umask was in force. */
system('chmod 644 ' + shellQuote(path));
return true;
}
function resolve_env(dm) {
const routing_mode = dm.general.routing_mode || 'bypass_mainland_china';
const ubus = connect();
const env = {
wan_dns: (ubus?.call('network.interface', 'status', {'interface': 'wan'}))?.['dns-server']?.[0],
direct_domain_list: [],
@@ -81,9 +146,248 @@ function resolve_env(dm) {
* lstat() is used because it is unambiguous - there is no second
* argument to get wrong - and because the neighbouring stderr-size
* check in homeproxy-pro.uc already reads sizes through it. */
china_ip6_ready: lstat(HP_DIR + '/resources/china_ip6.json') !== null
china_ip6_ready: lstat(HP_DIR + '/resources/china_ip6.json') !== null,
/* Which enabled `type: local` rule-sets have a usable file on disk,
* keyed by UCI section name.
*
* The same boundary as china_ip6_ready above, and for the same reason
* it is resolved here rather than inside generator/: a rule-set whose
* `path` names a file that is not there is a filesystem question, and
* asking it from the generator would break the "generator/ is a pure
* function of its arguments" invariant guard 27 enforces.
*
* Only enabled local rule-sets are looked at, mirroring the filter
* build_user_rulesets() applies first - a disabled entry never reaches
* the generated configuration, so a missing file behind one is not an
* error. The status is three-way on purpose:
*
* missing key the file is not there, or not a regular file, or
* empty - all three make `sing-box check` fail, the
* first two with a filesystem error and the third
* with "invalid sing-box rule-set file"
* true present and non-empty
* never true deliberately absent for a disabled or non-local
* entry, so "not checked" is distinguishable from
* "checked and absent"
*
* A `{tag}` placeholder in a path is expanded to one file per tag
* before the stat, because that is what sing-box does with it: a
* multi-tag rule-set whose second tag has no file fails the same way
* the first one would. rule_set_tags() is imported from
* generator/common.uc so this and the generator agree on the tag
* names; the UI only offers extra_tags on remote rule-sets, so this
* path is reachable through a direct UCI write rather than the form.
*
* This is root's view of the filesystem, which is what the generator
* needs; whether the jailed sing-box user can read the file is the
* runtime's business (hp_prepare_runtime_files hands the archive over
* on every start). */
ruleset_local_ready: {},
/* What each rule-set's own file actually IS, keyed by UCI section
* name: { '<section>': 'binary' | 'source' }.
*
* A verdict about the bytes, so the generator can stop trusting a
* field that describes the file's NAME: sing-box infers `format`
* from the extension and is wrong in three ways that all end the
* same way (`sing-box check` rejects the configuration, the reload
* is aborted, the user sees "my change did not take") - content
* disagreeing with the name, the field naming the wrong format, and
* no extension to infer from at all. See ruleSetFormatFromBytes()
* in homeproxy-pro.uc for the whole argument.
*
* No entry means no opinion, and that is the normal case for a
* remote rule-set with no initial_path: its content comes from a URL
* and generation must not touch the network. A missing entry is
* therefore never read as "source" or "binary" - it is read as
* "leave the declared format alone", which is also what sing-box
* does with a rule-set nobody told anything about. */
ruleset_formats: {},
/* Which empty startup fallbacks are actually on disk, keyed by rule_set
* tag: { '<tag>': true }. Presence, not a path - see the block at the
* end of this function. */
ruleset_initial: {}
};
if (routing_mode === 'custom') {
for (let cfg in (dm.routing.rulesets || [])) {
if (!cfg.enabled)
continue;
/* The file sing-box opens at startup: a local rule-set's `path`,
* and a remote one's `initial_path` when it has one. Both are
* subject to the same questions (is it there, what is it), which
* is why one pass answers both.
*
* A remote rule-set with no initial_path has nothing to look at:
* its content is whatever the URL serves, and fetching that
* during generation is exactly what generation must not do. */
const source = (cfg.type === 'local') ? cfg.path
: ((cfg.type === 'remote') ? cfg.initial_path : null);
if (isEmpty(source))
continue;
/* One path per tag. A single-tag rule-set yields exactly one, so
* this is the plain case and the loop is the only complication -
* and it has to be here, because sing-box substitutes {tag} and
* opens EVERY resulting file. rule_set_tags() is the shared tag
* list (see common.uc): a second copy would be free to drift,
* and the drift would be silent - a pre-check that stats files
* the running configuration never names. */
const paths = [];
if (match(source, /\{tag\}/))
for (let tag in rule_set_tags(cfg))
push(paths, replace(source, '{tag}', tag));
else
push(paths, source);
/* Both answers below are about paths the sing-box jail opens as
* the sing-box user, so an out-of-policy path is not read at all
* here. ruleset.uc refuses it a moment later with the message
* the user can act on; producing no opinion about a file this
* process has no business opening is the whole point of the
* check. */
let in_policy = true;
for (let p in paths)
if (!validateRuleSetPath(p)) {
in_policy = false;
break;
}
if (!in_policy)
continue;
/* Presence: every tag's file, and "present" means a non-empty
* regular file - all three states make `sing-box check` fail,
* the first two with a filesystem error and the third with
* "invalid sing-box rule-set file". Only a local rule-set
* consults this; a remote one is allowed to have no
* initial_path, which is the normal case. */
let all_present = true;
for (let p in paths) {
const st = lstat(p);
if (!st || st.type !== 'file' || st.size <= 0) {
all_present = false;
break;
}
}
if (cfg.type === 'local' && all_present)
env.ruleset_local_ready[cfg.name] = true;
/* Content: one verdict, and only when every file agrees.
*
* A disagreement between tags, or a file the probe cannot
* classify (a truncated download, an HTML error page saved
* under a .srs name), yields no verdict at all rather than the
* first answer that came back. Correcting on ambiguous
* evidence is how an auto-correction turns into a second source
* of wrongness, and sing-box's own error is a better one than a
* guess dressed up as a fix. */
let verdict = null, unanimous = true;
for (let p in paths) {
const seen = probeRuleSetFile(p);
if (!seen) {
unanimous = false;
break;
}
if (!verdict)
verdict = seen;
else if (verdict !== seen) {
unanimous = false;
break;
}
}
if (unanimous && verdict)
env.ruleset_formats[cfg.name] = verdict;
}
}
/* ruleset_safe_start: write the empty fallbacks.
*
* Recorded as PRESENCE, not as a path: env.ruleset_initial[tag] is true
* only once a file for that tag is on disk and readable. The generator
* then emits `initial_path` for exactly the tags it can point at, which
* is what keeps the two sides from disagreeing - a file that could not be
* written is simply not offered, and that rule-set keeps today's
* behaviour (fetched during initialization) instead of pointing at
* something that is not there. sing-box ignores a missing initial_path
* and blocks startup exactly as if none had been configured, so this
* cannot make a broken install worse - but the point of the opt-in is
* that it helps, and a pointer to a nonexistent file helps nobody.
*
* Built-ins first, then the user's own. Both come from the same helper
* the generator uses, so the format the file was written in and the
* format declared in the config are decided by one question asked once. */
if (dm.general.ruleset_safe_start === '1') {
if (declaresBuiltinRemoteRuleSets(routing_mode)) {
for (let rs in BUILTIN_REMOTE_RULE_SETS) {
const path = ruleSetInitialFallback([ rs.tag ], rs.format, rs.url);
if (path && writeEmptyRuleSet(path, rs.format))
env.ruleset_initial[rs.tag] = true;
else
warn(sprintf("homeproxy-pro: could not write the empty fallback for '%s'; this rule-set will be fetched during startup, as before.", rs.tag));
}
}
/* User rule-sets are custom-mode only - build_user_rulesets() is not
* called in any other mode - so writing fallbacks for them anywhere
* else would create files nothing ever points at. A rule-set with an
* initial file of its own is left completely alone. */
if (routing_mode === 'custom') {
for (let cfg in (dm.routing.rulesets || [])) {
if (!cfg.enabled || cfg.type !== 'remote' || !isEmpty(cfg.initial_path))
continue;
const format = cfg.format || ruleSetFormatFromPath(cfg.url);
const tags = rule_set_tags(cfg);
const template = ruleSetInitialFallback(tags, format, cfg.url);
if (!template) {
warn(sprintf("homeproxy-pro: rule-set '%s' has no usable format for the startup fallback (format '%s', url '%s'); it will be fetched during startup, as before.", cfg.name, cfg.format || '', cfg.url || ''));
continue;
}
for (let tag in tags) {
if (env.ruleset_initial[tag])
continue;
/* A single-tag rule-set's template IS the path; a
* multi-tag one carries {tag} and needs one file each -
* sing-box opens every tag's file, so a missing second
* one blocks startup as surely as a missing first. */
const path = (length(tags) > 1) ? replace(template, '{tag}', tag) : template;
if (writeEmptyRuleSet(path, format))
env.ruleset_initial[tag] = true;
else
warn(sprintf("homeproxy-pro: could not write the empty fallback for rule-set '%s' (tag %s); it will be fetched during startup, as before.", cfg.name, tag));
}
}
}
}
/* One line naming what is running on an empty fallback.
*
* The CLI already warns per rule-set when a fallback could not be written,
* but "which rule-sets are currently on an EMPTY one" is the fact a user
* needs and nothing else records it: the running configuration is not
* readable by the browser, the health gate does not look at rule-sets, and
* sing-box's own log only says the download failed. This line is the
* durable answer, and it goes to homeproxy-pro.log, which the status page can
* read with the permission it already has.
*
* Emitted only when the opt-in is on AND something actually got a
* fallback, so the log does not gain a line on every generation of a
* configuration that is not using the feature. */
if (dm.general.ruleset_safe_start === '1' && !isEmpty(env.ruleset_initial)) {
const tags = [];
for (let tag in env.ruleset_initial)
push(tags, tag);
warn(sprintf("homeproxy-pro: ruleset_safe_start is on; these rule-sets have an EMPTY initial file and match nothing until their download succeeds: %s.", join(', ', tags)));
}
if (routing_mode !== 'custom') {
const direct_list_raw = readfile(HP_DIR + '/resources/direct_list.txt');
env.direct_domain_list = direct_list_raw ? split(trim(direct_list_raw), /[\r\n]/) : [];
@@ -120,6 +120,68 @@ export function get_ruleset(cfg, dm) {
return rules;
};
/* rule_set_tags(cfg) - the rule_set tags ONE `dm.routing.rulesets` section
* produces: `cfg-<name>-rule`, plus one per entry in extra_tags.
*
* The generator needs it to build the tag; the CLI's environment resolution
* needs it too, because a `{tag}` placeholder in a source path has to be
* expanded to the real per-tag files before their existence can be checked.
* Two copies of this list would be free to drift, and the failure mode is
* quiet: the pre-check would stat files the running configuration never
* names, and a genuinely missing one would go unreported.
*
* Declared after get_ruleset() but before its callers for the same reason that
* function carries its own note: ucode resolves references at call time from
* what has already been evaluated. */
export function rule_set_tags(cfg) {
const tags = [ 'cfg-' + cfg.name + '-rule' ];
for (let t in (cfg.extra_tags || []))
push(tags, 'cfg-' + t + '-rule');
return tags;
};
/* BUILTIN_REMOTE_RULE_SETS - the remote rule-sets the generator declares
* itself, in the modes that split on mainland China.
*
* geoip-cn referenced by the route rule to split destinations by IP
* geosite-cn referenced only by the DNS rule (server: china-dns); the
* route layer never matches it, but sing-box still loads and
* keeps it, so removing it would break the DNS split
*
* This list is shared with generate_client.uc, which has to create the
* "do not block startup" fallback file for each of them before the generator
* runs. The two used to be independent: route.uc hard-coded the entries and
* the CLI knew nothing about them, which is why a cold-cache first start
* blocked on downloading them and why a change to either side could not be
* checked against the other. One list, two readers.
*
* Not a conffile-order dependency - this is the generator's own declaration,
* in the order the generated rule_set array must keep so the emitted bytes
* stay stable. */
export const BUILTIN_REMOTE_RULE_SETS = [
{
tag: 'geoip-cn',
format: 'binary',
url: 'https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs',
update_interval: '24h'
},
{
tag: 'geosite-cn',
format: 'binary',
url: 'https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs',
update_interval: '24h'
}
];
/* The routing modes that declare them. Kept beside the list so the CLI can
* decide whether it needs to create their fallbacks without repeating the
* condition route.uc branches on - and so the two cannot drift. */
export function declaresBuiltinRemoteRuleSets(routing_mode) {
return routing_mode === 'bypass_mainland_china' || routing_mode === 'proxy_mainland_china';
};
/* Resolve the direct-node destination override that the route builder
* needs for a `direct` routing_node target. The override is recorded
* earlier by generate_outbound() (see outbound.uc) when a direct node
@@ -138,6 +138,33 @@ export function build_context(dm, env) {
* whole config. Default false - an absent env must not turn into an
* optimistic "assume the file is there". */
china_ip6_ready: (env?.china_ip6_ready === true),
/* Which enabled `type: local` rule-sets have a usable file on disk,
* keyed by UCI section name: { '<section>': true }.
*
* Same shape of problem as china_ip6_ready and for the same reason it
* lives in the CLI's env rather than under generator/ (guard 27): the
* answer is a filesystem fact, and a `path` naming a file that is not
* there makes `sing-box check` reject the whole configuration with
* sing-box's own "parse rule-set[0]: open ...: no such file or
* directory" - which names neither the rule-set the user created nor
* the directory they are supposed to copy files into. The generator
* turns a missing entry into a message that names both (ruleset.uc).
*
* The default is pessimistic for the same reason: an env that forgot
* the field (the server path, a future caller) must not turn "no file"
* into "assume the file is there". */
ruleset_local_ready: (env?.ruleset_local_ready || {}),
/* What each rule-set's file actually is, keyed by UCI section name.
*
* Reaches the generator as an ordinary field for the same reason
* china_ip6_ready does - and with the same pessimistic default, which
* matters more here. For china_ip6_ready a missing field meant "no
* file"; here it means "no opinion about the content", and the
* generator must read that as "leave the declared format alone".
* Defaulting it to, say, 'source' would have every remote rule-set
* with no initial_path silently declared as source JSON, and a binary
* .srs fetched from a URL would then fail to parse at startup. */
ruleset_formats: (env?.ruleset_formats || {}),
self_mark, ntp_server, dns_port, mixed_port,
redirect_port, tproxy_port,
tun_name, tun_addr4, tun_addr6, tun_mtu,
@@ -229,6 +256,29 @@ export function build_context(dm, env) {
* Default '0' preserves the
* 300ms / default-list behaviour so an upgrade is invisible. */
sniffer_advanced_mode: dm.general.sniffer_advanced_mode || '0',
/* ruleset_safe_start: when '1' (opt-in), every enabled remote
* rule-set that has no initial_path of its own gets one, pointing at
* an EMPTY rule-set the generator writes before the config is
* checked. A remote rule-set with no initial file is fetched during
* initialization, before the inbounds bind, so on a cold cache the
* first start depends on the CDN being reachable - and on the node,
* because the fetch goes through http_clients.
*
* The fallback is empty on purpose and that is the trade: a rule-set
* with no rules matches nothing, so its traffic falls through to
* `final`. That is a routing change, not a robustness tweak, which
* is why the default is '0' and why the whole repository stays
* fail-loud by default. Guard 52 pins the default, the way guard 39
* pins sniffer_advanced_mode's. */
ruleset_safe_start: dm.general.ruleset_safe_start || '0',
/* Which empty startup fallbacks the CLI actually wrote, keyed by
* rule_set tag. Presence, not a path: the generator emits
* `initial_path` only for tags it can point at, so a fallback that
* could not be written leaves that rule-set on today's behaviour
* rather than pointing at a file that is not there. Pessimistic by
* construction - an env that forgot the field (the server path) has
* no fallbacks, and the server declares no remote rule-sets anyway. */
ruleset_initial: (env?.ruleset_initial || {}),
main_urltest_nodes: dm.general.main_urltest_nodes || [],
main_urltest_interval: dm.general.main_urltest_interval,
main_urltest_tolerance: dm.general.main_urltest_tolerance,
@@ -26,9 +26,9 @@
'use strict';
import { isEmpty, strToInt, strToTime, strToBool, parse_port, HP_DIR } from '../homeproxy-pro.uc';
import { isEmpty, strToInt, strToTime, strToBool, parse_port, ruleSetInitialFallback, HP_DIR } from '../homeproxy-pro.uc';
import { get_outbound, get_resolver, get_ruleset, get_direct_override } from './common.uc';
import { BUILTIN_REMOTE_RULE_SETS, declaresBuiltinRemoteRuleSets, get_outbound, get_resolver, get_ruleset, get_direct_override } from './common.uc';
/* --- shared initial block (every routing mode) ------------------------- */
@@ -216,8 +216,7 @@ function build_route_proxy(config, dm, ctx, direct_overrides) {
* to reach this point without it and sing-box refused the whole config
* with "initialize rule[3]: rule-set not found: geoip-cn", which the
* health gate turned into a rollback and an unproxied network. */
if (ctx.routing_mode === 'bypass_mainland_china'
|| ctx.routing_mode === 'proxy_mainland_china') {
if (declaresBuiltinRemoteRuleSets(ctx.routing_mode)) {
/*
* Fetched straight from the upstream SagerNet repositories and
* downloaded through the selected node. A direct fetch depends on
@@ -237,34 +236,47 @@ function build_route_proxy(config, dm, ctx, direct_overrides) {
* server: china-dns); the route layer never matches
* it. Sing-box still loads and keeps the rule-set in
* memory even when only one block references it, so
* removing it would also break the DNS split. It is
* only declared for bypass_mainland_china because that
* is the only mode whose DNS chain routes through
* china-dns.
* removing it would also break the DNS split.
*
* History: a third geosite-noncn used to be declared here as well,
* but no rule referenced it and sing-box still loaded and updated
* it daily, so it was removed (the comment above was rewritten
* during the 2026-09-29 review to clarify that geosite-cn is still
* in use - the DNS side - just not from this block).
*
* The list itself lives in common.uc, because the CLI has to create
* an "initial" fallback file for each of these before this generator
* runs - see the note on BUILTIN_REMOTE_RULE_SETS. That is the whole
* reason this block is a loop: these two are the rule-sets a cold
* install blocks on, so a change to them that only reached one of the
* two readers would put the fallback out of step with the config
* that points at it.
*/
push(config.route.rule_set, {
type: 'remote',
tag: 'geoip-cn',
format: 'binary',
url: 'https://v4.gh-proxy.org/https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs',
update_interval: '24h',
download_detour: 'main-out'
});
push(config.route.rule_set, {
type: 'remote',
tag: 'geosite-cn',
format: 'binary',
url: 'https://v4.gh-proxy.org/https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs',
update_interval: '24h',
download_detour: 'main-out'
});
for (let rs in BUILTIN_REMOTE_RULE_SETS) {
push(config.route.rule_set, {
type: 'remote',
tag: rs.tag,
format: rs.format,
url: rs.url,
update_interval: rs.update_interval,
/* Only with the opt-in on, and only for a fallback the CLI
* actually wrote. A remote rule-set with no initial_path is
* fetched during initialization, before the inbounds bind, so
* a cold cache makes the first start depend on the CDN - and
* on the node, since these go through http_clients. The
* fallback is an EMPTY rule-set, which changes routing for
* everything it would have matched, so it stays off unless
* the user asks for it.
*
* These two are the rule-sets a fresh install blocks on, so
* they are also the whole point of the feature: an earlier
* version of this block only knew how to build the entries,
* and the CLI had no idea they existed. */
initial_path: (ctx.ruleset_safe_start === '1' && ctx.ruleset_initial[rs.tag])
? ruleSetInitialFallback([ rs.tag ], rs.format, rs.url) : null,
download_detour: 'main-out'
});
}
/* Local, and not downloaded: it is generated from the same
* china_ip4.txt the firewall renders homeproxy_mainland_addr_v4
* from, so the two cannot drift apart. `type: local` is watched by
@@ -26,9 +26,81 @@
'use strict';
import { isEmpty, validateHomeProxyPath } from '../homeproxy-pro.uc';
import { isEmpty, rulePathRootsText, ruleSetFormatFromPath, ruleSetInitialFallback, strToTime, validateRuleSetPath } from '../homeproxy-pro.uc';
import { get_outbound, isDirectOutboundTag } from './common.uc';
import { get_outbound, isDirectOutboundTag, rule_set_tags } from './common.uc';
/* --- the `format` a rule-set is emitted with ----------------------------- */
/* resolveFormat(cfg, ctx) -> 'binary' | 'source' | the declared value | null
*
* The declared `format` describes the file's NAME; sing-box infers it from
* the extension and is wrong in three ways that each reject the whole
* configuration at `sing-box check`, so the reload is aborted and the user
* sees only "my change did not take":
*
* 1. an explicit format that names the wrong one
* (`format: binary` on a file holding JSON) -> "invalid sing-box
* rule-set file"
* 2. no explicit format, but the content disagrees with the extension
* (example.srs holding JSON) -> the same error, and here the extension
* is what misled sing-box
* 3. no explicit format and no extension to infer from
* -> "route.rule_set[0]: missing format", a different error for the same
* underlying mistake
*
* ctx.ruleset_formats carries what the file's bytes actually are, resolved by
* the CLI (guard 27 keeps generator/ off the filesystem). All three cases are
* answered by comparing it against what the current arrangement would produce:
*
* - no verdict -> emit the declared value untouched. The
* dominant case (any remote rule-set), and the one that must stay a
* no-op: sing-box keeps being the authority.
* - declared, and wrong -> the verdict wins, loudly.
* - undeclared, and the extension would have been right -> emit nothing.
* Adding an explicit value that says what the extension already said
* would change the generated bytes of every working rule-set for no
* behavioural gain.
* - undeclared, and the extension would have been wrong (or there is none)
* -> the verdict wins, loudly.
*
* The correction is announced either way. warn() reaches the user through
* $RUN_DIR/homeproxy-pro.log, which is the only place that can say "the field you
* filled in does not describe the file you pointed at" - and unlike the
* reload's own message, it names the rule-set.
*/
function resolveFormat(cfg, ctx) {
const declared = cfg.format;
const probed = ctx.ruleset_formats[cfg.name];
if (!probed)
return declared;
/* The extension sing-box would have inferred from. initial_path is the
* fallback because a remote rule-set with no `path` still has a file the
* verdict is about. */
const inferred = ruleSetFormatFromPath(cfg.path || cfg.initial_path);
if (declared) {
if (declared === probed)
return declared;
warn(sprintf("homeproxy-pro: rule-set '%s' declares format '%s' but its file is %s; using '%s'.", cfg.name, declared, probed, probed));
return probed;
}
if (inferred === probed)
return null;
if (inferred) {
warn(sprintf("homeproxy-pro: rule-set '%s' is named '*.%s' (which sing-box reads as %s) but its file is %s; declaring format '%s'.", cfg.name, inferred == 'source' ? 'json' : 'srs', inferred, probed, probed));
return probed;
}
/* No extension at all: sing-box has nothing to infer from and reports
* "missing format". The verdict is the only thing that can save it. */
return probed;
};
/* --- user-defined rule_set entries (custom mode only) ----------------- */
@@ -41,11 +113,15 @@ export function build_user_rulesets(rule_set_array, dm, ctx) {
continue;
const extra_tags = cfg.extra_tags || [];
let rs_tag = 'cfg-' + cfg.name + '-rule';
/* One source of truth for the tag list, shared with the CLI that has
* to expand a {tag} placeholder before it can check that the files
* exist (common.uc's rule_set_tags). A second copy would be free to
* drift, and the failure would be quiet: the pre-check would stat
* files the running configuration never names. */
const all_tags = rule_set_tags(cfg);
let rs_tag = all_tags[0];
if (length(extra_tags) && cfg.type !== 'inline') {
rs_tag = [rs_tag];
for (let t in extra_tags)
push(rs_tag, 'cfg-' + t + '-rule');
rs_tag = all_tags;
/* sing-box 1.14: multi-tag requires a {tag} placeholder in the fetch source
(remote: url and initial_path, local: path). A missing
placeholder makes sing-box try to literal-substitute the
@@ -61,24 +137,65 @@ export function build_user_rulesets(rule_set_array, dm, ctx) {
die(sprintf("homeproxy-pro: rule-set '%s' uses extra tags but its initial_path lacks a {tag} placeholder.", cfg.name));
}
/* Local rule-set path is read by sing-box as root. The whitelist
* is enforced here because the LuCI form's datatype='file' is
* only UX - UCI can be set from anywhere on the LAN, and an
* arbitrary /etc/passwd would leak the file to anyone who
* could write UCI. die() early (same as get_resolver on a
* missing/disabled dns_server): silently dropping path left
* sing-box checking whatever the field evaluated to, and the
* user only saw the reload keep the previous config. */
if (cfg.type === 'local' && cfg.path && !validateHomeProxyPath(cfg.path))
die(sprintf("homeproxy-pro: rule-set '%s' path '%s' is outside the homeproxy-pro whitelist; choose a path under /etc/homeproxy-pro/.", cfg.name, cfg.path));
/* A local rule-set is opened by sing-box as root, so its path is a
* file-disclosure surface: the LuCI form's datatype='file' is only
* UX, and UCI can be set from anywhere on the LAN. The policy is
* RULE_PATH_ROOTS (homeproxy-pro.uc) - the archive this package creates
* at install time - and it is deliberately NARROWER than the general
* validateHomeProxyPath() gate, which also admits /tmp/homeproxy_*
* upload staging and the package's own resource lists.
*
* die() early, for the reason get_resolver() gives: dropping the
* field silently left sing-box checking whatever the field evaluated
* to, and the user only saw the reload keep the previous config. */
if (cfg.type === 'local' && !isEmpty(cfg.path)) {
if (!validateRuleSetPath(cfg.path))
die(sprintf("homeproxy-pro: rule-set '%s' path '%s' is outside the allowed rule-set roots (%s); move the file into one of them, or point the path at a file that is already there.", cfg.name, cfg.path, rulePathRootsText()));
/* The file has to be there too. `sing-box check` runs over this
* configuration before it is ever started, and it opens every
* local rule_set path - so a rule-set pointing at a file nobody
* has put there produced sing-box's own
*
* parse rule-set[0]: open /etc/homeproxy-pro/ruleset/x.srs: no such file
*
* which names neither the rule-set nor anything the user can act
* on, and surfaced to the user as "new client configuration is
* invalid, reload aborted". The archive is created at install
* time and before every generation precisely so that this is a
* "you have not copied the file yet" message instead of a raw
* filesystem error.
*
* The stat is root's view, taken by the one layer allowed to
* touch the filesystem (guard 27) and handed over as
* ctx.ruleset_local_ready. Whether the *jailed* sing-box user
* can read the file is the runtime's job:
* hp_prepare_runtime_files hands the archive to that user on
* every start. */
if (!ctx.ruleset_local_ready[cfg.name])
die(sprintf("homeproxy-pro: rule-set '%s' points at '%s', which is missing, not a regular file, or empty. Put the file in %s and reload.", cfg.name, cfg.path, rulePathRootsText()));
}
const ruleset = {
type: cfg.type,
tag: rs_tag,
format: cfg.format,
format: resolveFormat(cfg, ctx),
path: cfg.path,
url: cfg.url,
update_interval: cfg.update_interval
/* sing-box parses this as a Go duration, which has no bare-number
* form: `update_interval: "3600"` is a hard `time: missing unit
* in duration "3600"` that rejects the whole configuration. That
* one is measured, on 1.14.2. strToTime() fixes it for the bare
* number without inventing a unit table.
*
* It cannot fix a wrong unit, and the placeholder the form used
* to show ("1d") is one: Go's duration units are ns/us/ms/s/m/h
* and there is no day - which is why the generator's own
* built-in rule-sets say 24h. That is a claim about Go's parser
* rather than a measurement of sing-box, so the frontend rejects
* the shapes Go cannot read instead of relying on this comment
* being right. */
update_interval: strToTime(cfg.update_interval)
};
/* download_detour is a pre-1.14 option that only makes sense for
remote rule-sets; emitting it for local/inline ones makes sing-box
@@ -86,10 +203,49 @@ export function build_user_rulesets(rule_set_array, dm, ctx) {
right below. */
if (cfg.type === 'remote')
ruleset.download_detour = get_outbound(cfg.outbound, dm) || get_outbound(ctx.default_outbound, dm);
if (cfg.type === 'remote' && !isEmpty(cfg.initial_path))
/* initial_path is read off the local disk as root; same
* whitelist as the local ruleset path. */
ruleset.initial_path = validateHomeProxyPath(cfg.initial_path) ? cfg.initial_path : null;
/* initial_path is read off the local disk as root, so it answers to
* the same rule-set policy as `path` - and it fails LOUD rather than
* quietly becoming null. The old form (`... ? value : null`) was the
* last asymmetry left in this file: an out-of-policy initial_path was
* dropped, the download went back to blocking startup, and the user
* was left with a configuration that looked configured and behaved as
* if the field were empty. */
if (cfg.type === 'remote' && !isEmpty(cfg.initial_path)) {
if (!validateRuleSetPath(cfg.initial_path))
die(sprintf("homeproxy-pro: rule-set '%s' initial_path '%s' is outside the allowed rule-set roots (%s); move the file into one of them, or clear the field to let sing-box fetch the rule-set on its own.", cfg.name, cfg.initial_path, rulePathRootsText()));
ruleset.initial_path = cfg.initial_path;
} else if (cfg.type === 'remote' && ctx.ruleset_safe_start === '1') {
/* No initial file of the user's own, and the opt-in is on: point at
* the empty fallback the CLI wrote before this generator ran.
*
* ctx.ruleset_initial records PRESENCE, not a path, and every tag
* has to be present. sing-box substitutes {tag} and opens every
* resulting file, so a multi-tag rule-set whose second file is
* missing blocks startup exactly as surely as one whose first is -
* measured on 1.14.2, where a literal "{tag}.srs" on disk makes
* it fall back to downloading and fail. All-or-nothing is the
* only correct answer here.
*
* Nothing is emitted when the answer is no: the rule-set then
* behaves exactly as it does today, which is also what happens if
* the CLI could not write the file. */
const fallback = ruleSetInitialFallback(all_tags, cfg.format, cfg.url);
if (fallback) {
let ready = true;
for (let t in all_tags)
if (!ctx.ruleset_initial[t]) {
ready = false;
break;
}
if (ready)
ruleset.initial_path = fallback;
else
warn(sprintf("homeproxy-pro: rule-set '%s' has no complete empty fallback, so it will be fetched during startup as before.", cfg.name));
}
}
push(rule_set_array, ruleset);
}
};
@@ -118,6 +118,330 @@ export function validateCertificatePath(p) {
return false;
};
/* Rule-set source files: the `path` of a `type: local` rule_set and the
* `initial_path` of a `type: remote` one. sing-box opens both as root, so
* the threat model is the certificate one - an arbitrary UCI value would leak
* the file to anyone who can write the UCI tree from anywhere on the LAN.
*
* Unlike a certificate, a rule-set has exactly one home: the archive this
* package creates at install time (uci-defaults/luci-homeproxy-pro) and before
* every generation (runtime/service.sh's hp_prepare_ruleset_dir). So the
* policy is a single root rather than the three a certificate needs, and it
* is deliberately NARROWER than validateHomeProxyPath() - that gate also
* accepts /tmp/homeproxy_*, which exists for upload staging, and every path
* under /etc/homeproxy-pro/ including the resource lists the package itself
* rewrites. A rule-set belongs in the archive (issue #7, review 4.4/4.7).
*
* Kept as a third policy rather than folded into validateHomeProxyPath() for
* the reason validateCertificatePath() is: the policies have to be
* independently narrowable, and folding them back together is exactly how the
* certificate one regressed once already - a path the UI offered was accepted
* there and silently dropped here, so the TLS listener failed with nothing
* pointing at the path.
*
* The list is mirrored by HP_RULE_PATH_ROOTS in
* htdocs/luci-static/resources/homeproxy-pro.js; guard 50 in tests/arch-guard.sh
* keeps the two in step across the JS/ucode boundary, which nothing else can
* see. */
export const RULE_PATH_ROOTS = ['/etc/homeproxy-pro/ruleset/'];
/* validateRuleSetPath(p) - the rule-set path gate.
*
* Same shape as validateCertificatePath(): reject traversal and relative
* paths, then require one of RULE_PATH_ROOTS. A bare root ("/etc/homeproxy-pro/
* ruleset/" with nothing after it) is rejected - it names the directory, not
* a file. */
export function validateRuleSetPath(p) {
if (!p || type(p) !== 'string')
return false;
/* Reject traversal *before* the prefix checks: a plain prefix comparison
* accepts '/etc/homeproxy-pro/ruleset/../../etc/shadow', and sing-box reads
* these paths as root. */
if (match(p, /(^|\/)\.\.(\/|$)/))
return false;
/* Reject anything that does not start with '/' - a relative path in
* sing-box resolves against the process CWD. */
if (substr(p, 0, 1) !== '/')
return false;
for (let root in RULE_PATH_ROOTS)
if (length(p) > length(root) && substr(p, 0, length(root)) === root)
return true;
return false;
};
/* RULE_PATH_ROOTS rendered for a diagnostic.
*
* join(', ', RULE_PATH_ROOTS) would do this in one line - it is the form
* firewall_utils.uc uses for its own lists, and ucode's join() does flatten an
* array argument rather than stringifying it. The loop is kept anyway: it
* cannot be wrong about the separator, and this string ends up inside a die()
* a user reads in a log file. */
export function rulePathRootsText() {
let text = '';
for (let root in RULE_PATH_ROOTS)
text = text ? (text + ', ' + root) : root;
return text;
};
/* --- rule-set format probing ---------------------------------------------
*
* A rule-set comes in exactly two formats, and sing-box needs to be told
* which one it is looking at:
*
* source a JSON document, {"version":3,"rules":[...]}
* binary a compiled .srs, which starts with the three ASCII bytes "SRS"
*
* The field is optional, because sing-box infers it from the file extension
* (.json -> source, .srs -> binary). That inference is a guess about the
* NAME, and it is wrong in three ways that all end the same way - `sing-box
* check` rejects the whole configuration, so a reload is aborted and the user
* is left with "my change did not take":
*
* content disagrees with the name example.srs holding JSON -> the file
* is parsed as SRS and fails
* the field says the wrong thing format=binary on a .json -> same
* there is no name to infer from a file with no extension -> "missing
* format", a different error for the
* same underlying mistake
*
* So the correction is to look at the bytes. The functions below are split
* in two on purpose: ruleSetFormatFromBytes() is a pure function of what it
* is handed, so the decision is unit-testable and auditable, and
* probeRuleSetFile() is the only part that touches the disk. It is called
* from the CLI's resolve_env(), never from generator/ (guard 27), and its
* verdict reaches the generator as an ordinary context field.
*
* Nothing here ever *guesses*: an unreadable, empty or unclassifiable file
* yields no verdict at all, and the declared format is left exactly as the
* user wrote it. sing-box stays the authority, and auto-correction can
* never become a second source of wrongness. */
/* How many leading bytes ruleSetFormatFromBytes() needs. Three for the magic,
* plus room to skip leading whitespace before the first JSON brace - a file
* written by a text editor or a Windows tool routinely starts with "\r\n".
* 512 is far more than the decision can use and far less than any real
* rule-set, so reading it is one short read, not a load. */
export const RULESET_PROBE_BYTES = 512;
/* ruleSetFormatFromBytes(head, size) -> 'binary' | 'source' | null.
*
* `head` is the leading bytes as a string (ucode fs.read hands them over as
* one), `size` the whole file's size. Returns null when the bytes do not
* identify a format - an empty file, or content that is neither SRS nor JSON
* (a truncated download, an HTML error page served with a .srs name). */
export function ruleSetFormatFromBytes(head, size) {
/* Empty is not "source" and not "binary": sing-box rejects a 0-byte
* local rule-set with "invalid sing-box rule-set file" whatever format
* is declared, so no verdict is the honest answer here and declaring one
* would not help. */
if (!size || !head)
return null;
/* The SRS magic, compared byte by byte.
*
* isBinary() must NOT be reused here, and the reason is worth being exact
* about, because the obvious one is wrong: isBinary() does not say "not
* SRS", so it cannot be used as a negative test. It answers a different
* question - "does this look like text?" - and for rule-sets that answer
* is only *correlated* with the format, never equal to it:
*
* a full .srs happens to read as binary, because the version byte and
* the deflate stream carry control bytes. That is a property of this
* week's encoder, not of the format, and it only holds because the
* probe happens to read enough of the file;
* a negative is ambiguous - binary-but-not-SRS, or text that is not
* JSON - and those need different answers.
*
* The magic is the format's own identity, costs three bytes, and gives
* null for anything it does not recognise. That last part is the point:
* this probe declines to have an opinion rather than guessing. */
if (length(head) >= 3 &&
ord(head, 0) == 0x53 && ord(head, 1) == 0x52 && ord(head, 2) == 0x53)
return 'binary';
/* Source is JSON, so the first byte that is not whitespace decides.
* Both { (an object) and [ (an array) are accepted: sing-box's own
* source form is an object, but an array is still "this is JSON" and
* refusing to say so would only hand the decision back to a guess. */
const lead = trim(head);
if (length(lead) && (substr(lead, 0, 1) == '{' || substr(lead, 0, 1) == '['))
return 'source';
return null;
};
/* ruleSetFormatFromPath(path) -> 'binary' | 'source' | null.
*
* What sing-box's extension inference would decide for this name, expressed
* as the same two values. null means "no extension sing-box can infer from",
* which is the case that produces "missing format" rather than a wrong parse -
* and the reason a content probe is worth doing at all.
*
* Deliberately NOT a general extension parser: only the two suffixes sing-box
* acts on are named, so this cannot drift into claiming it knows sing-box's
* rules. A {tag} placeholder does not affect the answer, since the suffix
* comes after it. */
export function ruleSetFormatFromPath(path) {
if (!path || type(path) !== 'string')
return null;
if (match(path, /\.json$/))
return 'source';
if (match(path, /\.srs$/))
return 'binary';
return null;
};
/* --- the "do not block startup on the first download" fallback ---------- *
*
* A remote rule-set with no `initial_path` is fetched during
* initialization, BEFORE the inbounds bind. On a cold cache that means a
* fresh install cannot start until raw.githubusercontent.com answers - and if
* it does not, the whole instance dies with a FATAL and the health gate turns
* that into a rollback or a released intercept layer. The node has to be up
* too, because these downloads go through http_clients, so a working
* configuration can be kept off the air by an unreachable CDN.
*
* `initial_path` is the way out: sing-box reads the file first, starts
* immediately, and refreshes from the URL in the background. A download that
* keeps failing then becomes an ERROR line in the log instead of a failed
* start - and the health gate does not look at rule-sets at all, so a
* degraded rule-set is promoted like any other healthy start.
*
* What the file holds is an EMPTY rule-set, and that is the whole trade: a
* rule-set with no rules matches nothing, so its traffic falls through to
* `final`. That changes routing, so this is opt-in (config.ruleset_safe_start,
* default off) and never the default.
*
* The format has to match the declared one. A binary rule-set fed a source
* JSON is not read at all - sing-box ignores the mismatch and blocks exactly
* as it does with no initial_path - so the decision of whether a fallback is
* even possible is made once, here, from the same inputs on both sides. */
/* Where the generated fallbacks live. Inside the rule-set archive, so
* validateRuleSetPath() admits it and the jail's read-only mount of HP_DIR
* already exposes it to the sing-box user. */
export const RULESET_INITIAL_DIR = HP_DIR + '/ruleset/initial';
/* The two files the package ships for this. empty.source.json is what
* `sing-box rule-set compile` consumes (24 bytes, readable, and the form
* every version agrees on); empty.srs is the compiled result of THAT source
* on 1.14.2, kept as the last resort for a router where the compile cannot
* run.
*
* The .srs is committed rather than built at package time on purpose: it is a
* 14-byte binary, and having it in the tree means `hexdump -C` on a router
* can confirm byte for byte that the shipped constant is the documented one.
* It is deliberately NOT a conffile - it is ours, not the user's, and an
* upgrade may replace it freely.
*
* Compatibility note, since a hard-coded binary artifact invites the question:
* sing-box only rejects an SRS whose version is NEWER than its own, so a
* version-2 file stays readable. The compile path exists anyway, because a
* file produced by the kernel that is running it cannot drift from it. */
export const RULESET_EMPTY_SOURCE = RULESET_INITIAL_DIR + '/empty.source.json';
export const RULESET_EMPTY_BINARY = RULESET_INITIAL_DIR + '/empty.srs';
/* A tag becomes a file name, so it has to be one. UCI section names are
* already restricted, but a rule_set `tag` is a sing-box identifier that UCI
* does not fully own, and this value is about to be concatenated into a path
* the generator writes as root. Anything outside this set is refused, and
* the caller skips the fallback rather than sanitising it into a name that
* could collide with another rule-set's. */
export function isSafeRuleSetTag(tag) {
if (!tag || type(tag) !== 'string')
return false;
return match(tag, /^[A-Za-z0-9][A-Za-z0-9._-]*$/) != null;
};
/* The extension a format is stored under. Null for anything that is not one
* of the two formats, which is the caller's signal that no fallback is
* possible rather than something to guess at. */
export function ruleSetFormatExtension(format) {
if (format === 'binary')
return 'srs';
if (format === 'source')
return 'json';
return null;
};
/* ruleSetInitialFallback(tags, declared, url) -> the initial_path to emit, or
* null when no fallback is possible.
*
* One function, called from both sides on purpose. The CLI asks it to
* decide which files to create and the generator asks it where to point
* `initial_path`; if they each worked it out separately they could disagree
* about the format, and a mismatch is the failure this whole mechanism exists
* to avoid - sing-box ignores a wrong-format initial file and blocks startup
* exactly as if there were none.
*
* `declared` is the rule-set's own `format`, which for a rule-set with no
* initial file is also the format the generator emits (nothing to probe, so
* nothing is corrected). With no declared format the URL's extension is the
* only remaining evidence; with neither, the answer is null and the rule-set
* keeps today's behaviour rather than being handed a file of the wrong shape.
*
* More than one tag gets a `{tag}` placeholder, because that is what sing-box
* substitutes and it requires every tag's file to exist - P3 on 1.14.2: a
* literal "{tag}.srs" on disk makes it try to download and block. */
export function ruleSetInitialFallback(tags, declared, url) {
const format = declared || ruleSetFormatFromPath(url);
const ext = ruleSetFormatExtension(format);
if (!ext)
return null;
for (let tag in (tags || []))
if (!isSafeRuleSetTag(tag))
return null;
const stem = (length(tags || []) > 1) ? '{tag}' : tags[0];
return RULESET_INITIAL_DIR + '/' + stem + '.' + ext;
};
/* probeRuleSetFile(path) -> 'binary' | 'source' | null.
*
* The disk half. Returns null - meaning "no opinion" - for a path outside the
* rule-set policy, a missing/empty/non-regular file, an unreadable one, and
* for content it cannot classify. The policy check is not redundant with
* ruleset.uc's: this runs BEFORE the generator, on paths that have not been
* vetted yet, and it is a reader.
*
* lstat() rather than the two-argument access() - see the note in
* generate_client.uc's china_ip6_ready about this ucode build answering only
* in its one-argument form. */
export function probeRuleSetFile(path) {
if (!validateRuleSetPath(path))
return null;
const st = lstat(path);
if (!st || st.type !== 'file' || st.size <= 0)
return null;
const f = open(path);
if (!f)
return null;
/* The `?? ''` is the same defensive read read_capped() uses: a short or
* failed read must not become a null the caller has to reason about. */
const head = f.read(RULESET_PROBE_BYTES) ?? '';
f.close();
return ruleSetFormatFromBytes(head, st.size);
};
/* Read at most `limit` bytes from a file, or '' when it does not exist.
* The cap is deliberate: a command's output is not trustworthy input. */
function read_capped(path, limit) {
@@ -307,6 +631,28 @@ export function redactReason(reason) {
* reason is wget's own stderr (whitespace collapsed, length-capped) so the
* caller can tell a DNS failure from a timeout or a TLS handshake error.
*/
/* fetchBinary() -> the path to invoke the fetch layer with.
*
* uclient-fetch is base OpenWrt, and it lives in /bin on every target measured
* so far - but "base package" is a packaging fact, not a path guarantee, and
* the whole point of the switch is not to hard-code an assumption that some
* buildroot can violate. So: the two known locations first, then PATH as the
* fallback, which is also what makes the test suite able to shadow it with a
* stub.
*
* Declared before wGETVerbose() on purpose - see the note above
* redactReason() about ucode not hoisting exported functions. */
export function fetchBinary() {
for (let p in [ '/bin/uclient-fetch', '/usr/bin/uclient-fetch' ]) {
/* One-argument access() is the only form this ucode build answers;
* see the china_ip6_ready note in generate_client.uc. */
if (access(p))
return p;
}
return 'uclient-fetch';
};
export function wGETVerbose(url, ua) {
if (!url || type(url) !== 'string')
return { content: null, error: 'invalid URL' };
@@ -314,37 +660,73 @@ export function wGETVerbose(url, ua) {
if (!ua)
ua = 'Wget/1.21 (HomeProxy, like v2rayN)';
/* -nv (not -q) so wget still reports *why* a fetch failed on stderr.
/* Why uclient-fetch and not wget: `wget` is whatever the firmware's
* buildroot happened to compile, and the two implementations do not share
* a single option beyond -O. A command line that works on one of them
* exits 2 on the other with "unrecognized option" *before making a single
* request*, which is how every subscription fetch, every resource-list
* update and the connectivity check failed on a busybox-wget router while
* the suite stayed green - the guard below only ever ran against GNU
* wget, on CI and on the maintainer's own device.
*
* The size cap is enforced by piping through `head -c`, NOT with wget's
* --max-filesize: that option does not exist in busybox wget (the target's
* /usr/bin/wget), which exits 2 with "unrecognized option" before making a
* single request - so every subscription fetch failed. GNU wget has no
* such option either. `head` closing the pipe stops wget early, which
* bounds the download; the cap is therefore CAP+1 bytes rather than
* exactly CAP, and one byte past the limit means "too large".
* uclient-fetch is the fetcher OpenWrt itself ships and drives (opkg,
* sysupgrade, uci), with an option set fixed by the applet rather than by
* the buildroot, so one command line is correct on every target. Its
* interface was read off the applet on the device, not from memory:
*
* -O <file> stdout is "-"
* --user-agent <str> -U
* --timeout=N | -T N seconds, same unit as the --timeout= it replaces
* --spider | -s existence check only
* --header='K: V' note the '=': the space-separated form is not
* accepted, and the value is ONE argv element
*
* No --quiet, deliberately: the point of capturing stderr is to report
* *why* a fetch failed, and uclient-fetch's default stderr is better than
* wget's -nv was - on an HTTP error it prints
*
* Downloading 'https://…/x.srs?token=…'
* Connecting to 185.199.111.133:443
* HTTP error 404
*
* including the resolved address. The URL is in there, so this depends on
* redactReason() below; guard 14 is what keeps that honest. Quiet mode
* would have removed the URL from the message and, with it, the only
* thing distinguishing an HTTP 404 from a connect failure.
*
* The size cap is still enforced by piping through `head -c`, not by a
* fetcher option: there is no such option in either implementation, and
* `head` closing the pipe stops the download early. The cap is therefore
* CAP+1 bytes rather than exactly CAP, and one byte past the limit means
* "too large".
*
* 5 MiB covers a 10 000-node subscription with ~3 KB per node plus the
* base64 inflation. Anything larger is almost certainly an attack or a
* misconfiguration.
*
* The pipeline does cost the exit status: `system()` returns head's, which
* is always 0. A wget failure therefore arrives as an empty body plus
* wget's own message on stderr, and that is reported below. */
* is always 0. A fetch failure therefore arrives as an empty body plus the
* fetcher's own message on stderr, and that is reported below. */
/* The braces matter: executeCommand() appends `>out 2>err` to the command,
* and in `a | b >out 2>err` those redirections bind to b only - wget's
* stderr would go to the caller's terminal and the failure message would
* be lost. Grouping the pipeline makes both stream to the capture files. */
const output = executeCommand(`{ /usr/bin/wget -nv -O- --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)} | head -c ${HP_FETCH_CAP + 1}; }`) || {};
* and in `a | b >out 2>err` those redirections bind to b only - the
* fetcher's stderr would go to the caller's terminal and the failure
* message would be lost. Grouping the pipeline makes both stream to the
* capture files.
*
* fetchBinary() is shellQuote()d like everything else here rather than
* being waved through on the grounds that it only ever returns a literal:
* guard 25 exists to make "is this shell-safe" a mechanical check instead
* of a judgement call, and a quoted constant costs nothing. */
const output = executeCommand(`{ ${shellQuote(fetchBinary())} -O - --user-agent=${shellQuote(ua)} --timeout=10 ${shellQuote(url)} | head -c ${HP_FETCH_CAP + 1}; }`) || {};
let reason = trim(output.stderr || '');
reason = reason ? replace(reason, /\s+/g, ' ') : '';
/* Review H3: an HTTP-level wget failure echoes the full URL - query
* string and subscription token included - as in
* `https://host/path?token=secret: 404 Not Found`. (A pure connection
* failure prints only `failed: Connection refused.` and leaks nothing,
* but the 404/403 case is enough.) Redact at the source so every caller
* of wGETVerbose gets a safe `error` whether or not it remembers to call
* redactUrl itself. */
/* An HTTP-level failure prints the requested URL back into the message
* with its query string and subscription token attached, as in
* `Downloading 'https://host/path?token=secret'` + `HTTP error 404`.
* (A pure connection failure prints only `Failed to send request: …` and
* leaks nothing, but the 404/403 case is enough.) Redact at the source
* so every caller of wGETVerbose gets a safe `error` whether or not it
* remembers to call redactUrl itself. */
if (reason)
reason = redactReason(reason);
@@ -355,15 +737,30 @@ export function wGETVerbose(url, ua) {
if (length(reason) > 200)
reason = substr(reason, 0, 200) + '...';
return { content: null, error: `wget exited with status ${output.exitcode}: ${reason || 'no error output'}` };
return { content: null, error: `fetch exited with status ${output.exitcode}: ${reason || 'no error output'}` };
}
/* head() masks wget's status, so a failed fetch shows up here instead. */
/* A binary body is not a failed fetch.
*
* executeCommand() nulls stdout for binary content - a subscription body
* that is not text cannot be parsed by anything downstream - and without
* this branch the "no content, but stderr said something" test below
* reported the result as
*
* fetch failed: Downloading '…' … Download completed (34185 bytes)
*
* which contradicts itself, and which would send a user looking for a
* network problem they do not have. Measured on a device, with a real
* .srs fetched from a URL that answered 200. */
if (output.binary)
return { content: null, error: 'the fetch succeeded but the response is binary, not a subscription payload' };
/* head() masks the fetcher's status, so a failed fetch shows up here. */
if (!length(trim(output.stdout)) && reason) {
if (length(reason) > 200)
reason = substr(reason, 0, 200) + '...';
return { content: null, error: `wget failed: ${reason}` };
return { content: null, error: `fetch failed: ${reason}` };
}
return { content: trim(output.stdout), error: null };
@@ -156,6 +156,39 @@ hp_clear_autoupdate_cron() {
/etc/init.d/cron restart >"/dev/null" 2>&1 || log "Warning: failed to restart cron."
}
# hp_prepare_ruleset_dir <hp-dir>
# Create the rule-set archive, /etc/homeproxy-pro/ruleset.
#
# Called BEFORE the client configuration is generated, not only from
# hp_prepare_runtime_files. Generation runs `sing-box check` over the result
# and check opens every local rule_set path, so on a fresh install the most
# natural path - "switch to custom routing, add a local rule-set, point it at
# /etc/homeproxy-pro/ruleset/example.srs" - was guaranteed to fail: hp_prepare_
# runtime_files only created the directory for a custom-mode start that had
# already got that far, and it runs after the generation, not before it. The
# user got sing-box's own
#
# parse rule-set[0]: open /etc/homeproxy-pro/ruleset/example.srs: no such file
#
# and the reload reported only "new client configuration is invalid, reload
# aborted" - with the directory the UI points at not existing yet.
#
# The directory is also created at install time (uci-defaults/luci-homeproxy-pro)
# so a router that has never started the service still has it; this call is
# what covers an upgrade from a version that predates that, and a user who
# removed it by hand.
#
# Ownership is NOT done here. The jailed client runs as the sing-box user and
# has to read the files, but it starts after hp_prepare_runtime_files, which is
# where the recursive chown lives; doing it here as well would only add a
# second pass over the directory on every start.
hp_prepare_ruleset_dir() {
local hp_dir="$1"
[ -d "$hp_dir/ruleset" ] || mkdir -p "$hp_dir/ruleset" \
|| log "Warning: failed to create ${hp_dir}/ruleset."
}
# hp_prepare_runtime_files <hp-dir> <run-dir> <routing-mode> <client> <server>
# Create the mode-specific working files, truncate the instance logs and hand
# every runtime file to the sing-box user. <client>/<server> are "1"/"0".
@@ -229,13 +262,17 @@ hp_prepare_runtime_files() {
esac
if [ "$routing_mode" = "custom" ]; then
[ -d "$hp_dir/ruleset" ] || mkdir -p "$hp_dir/ruleset" \
|| log "Warning: failed to create ${hp_dir}/ruleset."
# The directory itself is created earlier - see hp_prepare_ruleset_dir()
# for why generation cannot wait for this function. Re-running the
# idempotent mkdir here costs nothing and keeps this function
# self-contained for any caller that reaches it directly.
hp_prepare_ruleset_dir "$hp_dir"
# A local rule-set is opened by the client, which no longer runs as
# root now that the jail is on for this mode too, so the files have to
# be readable by the sing-box user. Only the documented directory is
# claimed here; a rule-set placed elsewhere under HP_DIR has to be
# readable already.
# be readable by the sing-box user. Only the documented archive is
# claimed here, and it is the only directory validateRuleSetPath()
# admits; a rule-set placed elsewhere under HP_DIR is not a rule-set
# the generator will emit any more.
chown -R sing-box:sing-box "$hp_dir/ruleset" 2>"/dev/null" \
|| log "Warning: failed to hand ${hp_dir}/ruleset to sing-box."
fi
@@ -61,7 +61,20 @@ pick_mirror() {
else
local probe_url="https://$base/gh/$listrepo@$list_sha/$listname"
fi
if wget --timeout=10 --spider -q "$probe_url" 2>"/dev/null"; then
# uclient-fetch, not wget. wget is whichever implementation the
# buildroot compiled, and the two share almost no options: on a
# busybox-wget router every flag here is rejected with "unrecognized
# option" before a request is made, so the resource lists silently
# never updated - pick_mirror returns 1 and the caller carries on.
# uclient-fetch is the fetcher OpenWrt itself uses and its option set
# is fixed by the applet, not by the buildroot. Read off the applet
# on the device: --spider checks existence only, --timeout=N is
# seconds. -q because this probe's stderr goes nowhere anyway.
#
# Resolved through PATH rather than spelled /bin/..., exactly as this
# script used to spell a bare `wget`: it keeps the name overridable in
# the test sandbox, which is how the option list gets exercised at all.
if uclient-fetch -q -s --timeout=10 "$probe_url" 2>"/dev/null"; then
printf '%s\n' "$base"
return 0
fi
@@ -76,7 +89,7 @@ check_list_update() {
local listname="$4"
local lock="$RUN_DIR/update_resources-$listtype.lock"
local github_token="$(uci -q get homeproxy-pro.config.github_token)"
local wget="wget --timeout=10 -q"
local fetch="uclient-fetch -q --timeout=10"
# fd 9, not 200: POSIX only guarantees 0-9, and dash fails the whole
# `exec 200>"$lock"` with "exec: 200: not found". busybox ash and bash
@@ -90,21 +103,23 @@ check_list_update() {
fi
# The token travels in argv, which the previous form avoided by writing it
# to a 0600 file and passing --header-file - an option neither busybox nor
# GNU wget has, so it failed with "unrecognized option" and the version
# query never worked at all with a token configured. There is no
# file-based header option in either wget, so argv is the only way; on a
# to a 0600 file and passing --header-file - an option no fetch
# implementation has, so it failed with "unrecognized option" and the
# version query never worked at all with a token configured. uclient-fetch
# has no file-based header option either, so argv is the only way; on a
# single-user router that is the right trade for a feature that otherwise
# does not function. `wget` is invoked with the header only when a token
# is set, and nothing logs the command line.
# does not function. The header is passed only when a token is set, and
# nothing logs the command line.
local github_header=""
[ -n "$github_token" ] && github_header="Authorization: Bearer $github_token"
local list_info="$($wget ${github_header:+--header "$github_header"} -O- "https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1")"
local wget_exit=$?
# --header takes '=': the space-separated form is rejected, and the value
# has to stay ONE argv element, which the quoting around it preserves.
local list_info="$($fetch ${github_header:+--header="$github_header"} -O- "https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1")"
local fetch_exit=$?
if [ $wget_exit -ne 0 ]; then
log "[$(to_upper "$listtype")] Failed to fetch version info (wget exit $wget_exit)."
if [ $fetch_exit -ne 0 ]; then
log "[$(to_upper "$listtype")] Failed to fetch version info (fetch exit $fetch_exit)."
return 1
fi
local list_sha="$(printf '%s' "$list_info" | jsonfilter -qe "@[0].sha")"
@@ -144,7 +159,7 @@ check_list_update() {
fi
log "[$(to_upper "$listtype")] Downloading from $mirror."
if ! $wget -O "$RUN_DIR/$listname" "$mirror_url" || [ ! -s "$RUN_DIR/$listname" ]; then
if ! $fetch -O "$RUN_DIR/$listname" "$mirror_url" || [ ! -s "$RUN_DIR/$listname" ]; then
rm -f "$RUN_DIR/$listname"
log "[$(to_upper "$listtype")] Download failed ($mirror)."
return 1
@@ -162,7 +177,7 @@ check_list_update() {
# resources before this check either; the alternative - installing bytes
# nobody vouched for - is the thing being fixed.
local api_blob local_blob
api_blob="$($wget ${github_header:+--header "$github_header"} -O- \
api_blob="$($fetch ${github_header:+--header="$github_header"} -O- \
"https://api.github.com/repos/$listrepo/contents/$listname?ref=$list_sha" \
| jsonfilter -qe '@.sha')"
if [ -z "$api_blob" ]; then
@@ -221,7 +221,7 @@ function main() {
/* Keep the lock's mtime current for as long as this run is alive.
* The stale window (LOCK_STALE) is what lets a later run reclaim a
* lock a killed process left behind, but nothing refreshed it while
* the fetch was in progress - and the fetch is one wget per URL,
* the fetch was in progress - and the fetch is one request per URL,
* ten seconds each. A run that crosses the threshold while still
* working can therefore have its lock broken underneath it, and the
* two runs then interleave their read-modify-write exactly like the
@@ -248,6 +248,14 @@ start_service() {
if [ "$client_enabled" = "1" ]; then
hp_net_wait_wan 60
# The rule-set archive has to exist before the generator runs, not
# after: `sing-box check` opens every local rule_set path, so a
# missing directory used to surface as sing-box's own
# "parse rule-set[0]: open ...: no such file" and nothing else. The
# generator also refuses a rule-set whose file is absent, with a
# message that names the rule-set and the directory to copy into.
hp_prepare_ruleset_dir "$HP_DIR"
if ! hp_start_generated_config c "$HP_DIR" "$RUN_DIR" "$GOOD_DIR"; then
hp_release_stale_intercept
return 1
@@ -511,6 +519,12 @@ reload_service() {
# aborted *before* stop_service() - the old configuration keeps running
# instead of the router ending up with no service at all.
#
# The rule-set archive is created before the preflight, for the same
# reason start_service does it: the generator's `sing-box check` opens
# every local rule_set path, so a missing directory would fail the
# preflight for a reason the user cannot act on.
hp_prepare_ruleset_dir "$HP_DIR"
#
# A3: whichever sides are generated here are the candidates start_service
# must activate - it must not generate them a second time. Two invocations
# of the generator are not guaranteed to write the same bytes (its env
@@ -3,6 +3,30 @@
[ -f "/www/luci-static/resources/icons/loading.gif" ] && \
sed -i "s,/loading.svg,/loading.gif,g" "/www/luci-static/resources/view/homeproxy-pro/status.js"
# The rule-set archive, created at install time.
#
# /etc/homeproxy-pro/ruleset is where the LuCI rule-set form points, and it is the
# only directory validateRuleSetPath() (homeproxy-pro.uc) admits for a local
# rule-set's `path` or a remote rule-set's `initial_path`. It used to be created
# only by a custom-mode start that had already got far enough, which was after
# the client configuration had been generated and `sing-box check`ed - so on a
# fresh install the natural first rule-set could not be added at all, and the
# only thing the user saw was "new client configuration is invalid, reload
# aborted". runtime/service.sh's hp_prepare_ruleset_dir() now creates it
# before every generation as well; this is what makes it exist on a router
# whose service has not started yet.
#
# Deliberately NOT a package conffile. The directory holds the user's own
# rule-set files, and conffiles are what the package manager overwrites on
# upgrade (arch-guard 33/37 pin that list against both packaging paths).
#
# mkdir -p is idempotent and the chown is best-effort: on a fresh install the
# sing-box user comes from a dependency, but a failure here must not abort the
# whole uci-defaults run - the directory existing is what matters, and
# hp_prepare_runtime_files hands ownership over on the first start anyway.
mkdir -p /etc/homeproxy-pro/ruleset
chown sing-box:sing-box /etc/homeproxy-pro/ruleset 2>"/dev/null"
uci -q batch <<-EOF >"/dev/null"
delete firewall.homeproxy_pre
@@ -10,7 +10,7 @@
import { access, error, lstat, popen, readfile, writefile } from 'fs';
import { cursor } from 'uci';
import { isBinary, isValidECHConfig, isValidPEM, shellQuote, executeCommand } from '/etc/homeproxy-pro/scripts/homeproxy-pro.uc';
import { fetchBinary, isBinary, isValidECHConfig, isValidPEM, shellQuote, executeCommand } from '/etc/homeproxy-pro/scripts/homeproxy-pro.uc';
import { parse_uri } from '/etc/homeproxy-pro/scripts/parser/uri.uc';
/* Kanged from ucode/luci - replaced by the imported shellQuote() so every shell
@@ -210,26 +210,35 @@ const methods = {
/* Probe the address family the configuration actually has.
*
* busybox wget has no Happy Eyeballs: it walks the resolved
* The fetcher has no Happy Eyeballs: it walks the resolved
* addresses one at a time, and `-T3` is spent on each. A target
* with seven AAAA records therefore takes over twenty seconds to
* fail, against the 3100 ms budget of the system() call below -
* so the button reported "failed" for Google while the same
* request over IPv4 succeeded, and Baidu passed only because its
* IPv6 goes out direct. The answer was about wget's retry order,
* not about whether the proxy worked.
* IPv6 goes out direct. The answer was about the fetcher's retry
* order, not about whether the proxy worked.
*
* Forcing the family makes the result mean what the label says.
* With IPv6 support on this is a real IPv6 test: it fails when the
* node cannot carry IPv6 egress, which is true information rather
* than a false negative, and it is the same condition a LAN client
* sees. With IPv6 support off, DNS hands out no AAAA at all, so
* -4 states explicitly what used to happen by accident. */
* -4 states explicitly what used to happen by accident.
*
* uclient-fetch rather than wget: wget is whichever implementation
* the buildroot compiled, and the two share almost no options, so
* a command line correct on one exits 2 on the other with
* "unrecognized option" - which on a busybox-wget router read as a
* FAILED PROXY rather than as a usage error. `--spider` is the
* existence check the old --spider meant and -T is the same three
* seconds. The 3100 ms system() budget is the outer bound; -T
* bounds the connect and request inside uclient-fetch. */
const uci = cursor();
const family = ((uci.get('homeproxy-pro', 'config', 'ipv6_support') || '0') === '1') ? 'IPv6' : 'IPv4';
return {
result: (system(`/usr/bin/wget -${(family === 'IPv6') ? '6' : '4'} --spider -qT3 ${shellQuote(url)} 2>"/dev/null"`, 3100) === 0),
result: (system(`${fetchBinary()} -${(family === 'IPv6') ? '6' : '4'} -q -s -T3 ${shellQuote(url)} 2>"/dev/null"`, 3100) === 0),
/* The backend already knows which family it probed, and it is the
* only layer that can know without the view re-reading UCI. The
* view shows this next to the verdict, which is where a "passed"
@@ -399,7 +408,7 @@ const methods = {
* (arch-guard 30).
*
* The previous synchronous shape returned the orchestrator's exit
* status and captured stderr, but the full pipeline (wget fetch
* status and captured stderr, but the full pipeline (the fetch
* through a CN -> overseas link, UCI commit, /etc/init.d/homeproxy-pro
* reload, the 30 s sing-box health gate) regularly took 10-15 s,
* which exceeded the browser XHR timeout on every browser we tried.
+3 -3
View File
@@ -299,7 +299,7 @@ Two further host differences are bridged so the remaining checks still run:
| `tests/runtime/test_dns_snippets.sh` | The dnsmasq snippet writer's incremental behaviour (review L6). Counts `init.d/dnsmasq restart` calls through a stub and asserts that an identical second write does *not* restart (a restart flushes every client's DNS cache), while a changed resource list, routing mode, DNS port or `ipv6_support` setting does. Also pins that a mode change drops the snippet the previous mode produced, and that removing a resource list removes its snippet so the next call is stable again. Pure shell, runs anywhere. |
| `tests/runtime/test_health_probe.sh` | The functional health probes (`runtime/health.sh`): `hp_probe_dns` / `hp_probe_tcp` answer 0 / 1 / 2 and keep "cannot probe here" (no tool, no port) distinct from "the probe failed"; `hp_run_probes` *reports* by default and only fails the gate when `health_probe_strict` is on, and never fails it for a probe that could not run; `hp_config_port_for_tag` reads the probe ports out of the running configuration in both the generator's pretty shape and the compact one. `nslookup` and `nc` are stubbed, so it is pure shell and runs anywhere. |
| `tests/runtime/test_crontab_perms.sh` | `hp_crontab_drop()` (runtime/service.sh) edits `/etc/crontabs/root` through a temporary file, and `mv` replaces the file rather than its contents - so the new one inherited the shell's umask (0644) while procd creates the root crontab as 0600. The test drives the function against a sandboxed crontab and pins: the entry is dropped, other jobs survive, the mode is 0600 (including a file an older version had already loosened), a missing crontab is reported as a failure and is not created. Pure shell, runs anywhere. |
| `tests/runtime/test_resource_update.sh` | The content-verification path of `scripts/update_resources.sh`: the download URL is pinned to a commit, and the file is installed only when its git blob id matches the one GitHub reports for that path and commit. Drives all four outcomes (match, mismatch, no digest from the API, no local digest) with `wget` / `jsonfilter` / `ucode` / `uci` / `flock` stubbed and the script's absolute paths rewritten into a sandbox, asserting in each failure case that the installed list and its `.ver` are left untouched - the point of the check is that it refuses, so a driver that only walked the happy path would keep passing with the check deleted. Pure shell, runs anywhere. |
| `tests/runtime/test_resource_update.sh` | The content-verification path of `scripts/update_resources.sh`: the download URL is pinned to a commit, and the file is installed only when its git blob id matches the one GitHub reports for that path and commit. Drives all four outcomes (match, mismatch, no digest from the API, no local digest) with `uclient-fetch` / `jsonfilter` / `ucode` / `uci` / `flock` stubbed and the script's absolute paths rewritten into a sandbox, asserting in each failure case that the installed list and its `.ver` are left untouched - the point of the check is that it refuses, so a driver that only walked the happy path would keep passing with the check deleted. Pure shell, runs anywhere. |
| `tests/runtime/test_runtime_extraction.sh` | Drives `init.d/homeproxy-pro` through a stubbed environment (fake `ip` / `nft` / `fw4` / `ucode` / `sing-box` / `uci` / `netstat` / `nslookup` / `nc`, fake procd and jsonfilter state, fixture UCI) across five scenarios and diffs the resulting command + file trace against a baseline. Two baselines exist and are captured with the SAME harness, so the diff between them is exactly the intentional change: `tests/fixtures/runtime/trace.pre-pr05.txt` (the 517-line init script before PHASE 7 moved the plumbing out — the record that the extraction was behaviour-preserving) and `tests/fixtures/runtime/trace.golden.txt` (after the health-gate fix). Scenario D is the P0 regression: a candidate whose `mixed_port` is already taken must be rejected by the health gate, the previous known-good must survive, and the reload must roll back onto it. The generated fixture configuration is pretty-printed like the generator's own `%.J` output, because the runtime reads that file (`hp_config_ports`, `hp_config_port_for_tag`) and a compact fixture would test a shape production never writes. Pure shell, no ucode needed. Regenerate with `HP_UPDATE_GOLDEN=1` (optionally `HP_GOLDEN=` / `HP_INITD=` to target another baseline or revision). |
**`HP_UPDATE_GOLDEN=1` is a baseline-update mode, not a test.** It copies the
@@ -351,7 +351,7 @@ module:
* `tests/ucode/mocks/homeproxy_fetcher.uc` — replaces `wGETVerbose` with a
stub that reads the canned response from a global, so the fetcher test never
shells out to `wget`.
shells out to `uclient-fetch`.
* `tests/ucode/mocks/homeproxy_firewall.uc` — `isEmpty` / `validation` plus a
`RUN_DIR` read from a global, so `firewall_pre.uc` writes its nft fragments
into a scratch dir.
@@ -369,7 +369,7 @@ harness provides:
| Path | Why |
| --- | --- |
| `scripts/update_resources.sh` | The version query and the download are a GitHub API round-trip plus a jsdelivr fetch, so the verification path is driven by `tests/runtime/test_resource_update.sh` with `wget` / `jsonfilter` / `ucode` / `uci` / `flock` stubbed and the script's absolute paths rewritten into a sandbox: matching digest installs, a mismatch or a missing digest refuses and leaves the installed list and its `.ver` alone. The `*)` usage branch is covered by the shell syntax check. |
| `scripts/update_resources.sh` | The version query and the download are a GitHub API round-trip plus a jsdelivr fetch, so the verification path is driven by `tests/runtime/test_resource_update.sh` with `uclient-fetch` / `jsonfilter` / `ucode` / `uci` / `flock` stubbed and the script's absolute paths rewritten into a sandbox: matching digest installs, a mismatch or a missing digest refuses and leaves the installed list and its `.ver` alone. The `*)` usage branch is covered by the shell syntax check. |
| `scripts/update_crond.sh` | A fixed list of invocations against hard-coded `/etc/homeproxy-pro/scripts` paths. Covered by the shell syntax check. |
| `scripts/clean_log.sh` | `while true; do sleep 180; …` — testing the rotation needs the loop made injectable first. Covered by the shell syntax check. |
| `htdocs/.../view/homeproxy-pro/status.js` | A LuCI view; its behaviour is only observable in a browser. |
+487 -7
View File
@@ -730,12 +730,22 @@ echo
echo "== guard 14: wGETVerbose redacts the URL at the source =="
# Review H3: the original fetcher.uc logged a redacted URL but returned the
# raw wget stderr, which still had the full URL (wget -nv reports the target
# on the failure line, query string and all). Every caller of wGETVerbose
# had to remember to redact the error themselves, and any that did not -
# silently leaked the subscription token. The fix moved redaction into
# wGETVerbose itself, so the returned `error` is safe no matter where the
# raw fetcher stderr, which still had the full URL. Every caller of
# wGETVerbose had to remember to redact the error themselves, and any that
# did not silently leaked the subscription token. The fix moved redaction
# into wGETVerbose itself, so the returned `error` is safe no matter where the
# caller ships it.
#
# The leak is not hypothetical under either fetcher: GNU wget -nv reports the
# target on the failure line, and uclient-fetch reports it by default, before
# it reports anything else -
#
# Downloading 'https://host/path?token=secret'
# HTTP error 404
#
# which is precisely why wGETVerbose does NOT pass -q. Quiet mode would have
# taken the URL out of the message and, with it, the only thing separating an
# HTTP 404 from a connect failure.
HOMEPROXY="$SCRIPTS/homeproxy-pro.uc"
FETCHER="$SCRIPTS/subscription/fetcher.uc"
@@ -2330,12 +2340,12 @@ echo "== guard 48: the connection check probes the configured address family =="
# IPv4 worked; Baidu passed only because its IPv6 goes out direct. Pin the two
# halves: the backend has to force the family, and the verdict has to say which
# one, or "passed" stays unfalsifiable.
if grep -qF "wget -\${(family === 'IPv6') ? '6' : '4'} --spider" "$RPC" &&
if grep -qF "\${fetchBinary()} -\${(family === 'IPv6') ? '6' : '4'} -q -s -T3" "$RPC" &&
grep -qF "uci.get('homeproxy-pro', 'config', 'ipv6_support')" "$RPC"; then
pass "connection_check forces the address family from homeproxy-pro.config.ipv6_support"
else
fail "connection_check does not force an address family - the result is whatever"
fail "busybox wget happens to try first, which is how a working proxy reads as failed"
fail "the fetcher happens to try first, which is how a working proxy reads as failed"
fi
if grep -qF "family: family" "$RPC"; then
@@ -2408,6 +2418,476 @@ else
fi
fi
echo
echo "== guard 50: the rule-set path policy is identical in both layers =="
# The rule-set form's `path` and `initial_path` were bare form.Value options
# with datatype='file', which in LuCI is `file() { return true; }` - so any
# string saved, and the generator was the first thing to object. Adding a
# frontend validator means the same class of bug guard 29 pins for
# certificates can now happen here instead: two lists, one per language, and
# nothing able to see across the JS/ucode boundary.
#
# The failure is asymmetric on purpose. A path the UI rejects but the backend
# accepts is a UI bug; a path the UI accepts but the backend drops is the
# dangerous one - the field vanishes from the generated configuration, the
# rule-set silently reverts to a blocking first download, and the user is left
# with a configuration that looks configured. The generator now also refuses
# loudly, but the UI check is what stops it at the field they are editing.
#
# Compared in BOTH directions, and also against the two places the policy has
# to stay coherent beyond the lists themselves: the message the validator
# shows, and the path the form offers as its placeholder / datalist entry. A
# placeholder naming a directory the validator refuses is the bug this guard
# exists for, wearing a different hat.
RULE_ROOTS="$(python3 - "$SCRIPTS/homeproxy-pro.uc" "$VIEWS/homeproxy-pro.js" "$VIEWS" <<'PY'
import re, sys
def roots(path, name, label):
text = open(path, encoding='utf-8').read()
m = re.search(r'\b%s\s*=\s*\[([^\]]*)\]' % name, text)
if not m:
return None, '%s: could not find %s' % (label, name)
return re.findall(r"'([^']+)'", m.group(1)), None
problems = []
backend, err = roots(sys.argv[1], 'RULE_PATH_ROOTS', 'homeproxy-pro.uc')
problems += [err] if err else []
frontend, err = roots(sys.argv[2], 'HP_RULE_PATH_ROOTS', 'homeproxy-pro.js')
problems += [err] if err else []
if not problems:
if backend != frontend:
problems.append('homeproxy-pro.uc %s != homeproxy-pro.js %s' % (backend, frontend))
elif not backend:
problems.append('RULE_PATH_ROOTS is empty - the rule-set gate would accept nothing')
else:
# Every root has to exist in the packaged tree, or the form points the
# user at a directory the package never creates. uci-defaults creates
# the archive at install time and hp_prepare_ruleset_dir() before every
# generation, so the literal root string has to be the one those two
# agree on - a rename of the archive in only one of the three places
# would leave the user with an unsavable form.
for src, label in ((sys.argv[1], 'homeproxy-pro.uc'), (sys.argv[2], 'homeproxy-pro.js')):
text = open(src, encoding='utf-8').read()
for r in backend:
if r not in text:
problems.append('%s does not mention the root %s' % (label, r))
# The offered default must be inside a declared root, and it must be
# the same string the frontend exposes as hp.rule_path_default.
js = open(sys.argv[2], encoding='utf-8').read()
m = re.search(r"\bHP_RULE_PATH_DEFAULT\s*=\s*'([^']+)'", js)
if not m:
problems.append('homeproxy-pro.js: could not find HP_RULE_PATH_DEFAULT')
elif not any(m.group(1).startswith(r) and len(m.group(1)) > len(r) for r in backend):
problems.append('HP_RULE_PATH_DEFAULT %r is not inside any RULE_PATH_ROOTS entry %s'
% (m.group(1), backend))
if not re.search(r'\brule_path_default:\s*HP_RULE_PATH_DEFAULT\b', js):
problems.append('homeproxy-pro.js: HP_RULE_PATH_DEFAULT is not exported as rule_path_default,'
' so the view cannot quote it and would repeat the literal')
# The validator the rule-set form binds must be the one that closes
# over this list. A form option pointing at the certificate validator
# would accept /etc/ssl/ and refuse the archive.
for view in ('client/subscription.js',):
body = open('%s/view/homeproxy-pro/%s' % (sys.argv[3], view), encoding='utf-8').read()
for field in ('path', 'initial_path'):
m = re.search(r"option\(form\.Value,\s*'%s'.*?(?=so = |\Z)" % field, body, re.S)
if not m:
problems.append('%s: no %s option found' % (view, field))
elif 'hp.validateRuleSetPath' not in m.group(0):
problems.append('%s: the %s option does not bind hp.validateRuleSetPath'
% (view, field))
print('\n'.join(problems))
PY
)" || RULE_ROOTS="__SCAN_FAILED__"
if [ "$RULE_ROOTS" = "__SCAN_FAILED__" ]; then
fail "the rule-set-roots scan could not run - fix the guard before trusting a pass"
elif [ -z "$RULE_ROOTS" ]; then
pass "the frontend and backend rule-set path roots agree, and the offered default is inside them"
else
fail "the rule-set path policy is not coherent across the layers:"
printf ' %s\n' "$RULE_ROOTS"
fi
echo
echo "== guard 51: the format probe stays a probe, and the form stops fighting it =="
# issue #7 batch 1 turned a comment into a mechanism: generation reads the first
# bytes of each rule-set file and declares `format` from that, and a bare
# `update_interval` is normalised before sing-box parses it as a Go duration.
#
# Every part of that is a decision that can be undone by a well-meaning edit,
# and each reversion reproduces a failure that is invisible until a router
# refuses to start:
#
# (a) the disk half of the probe moving under generator/ - the purity
# invariant guard 27 protects, but at a different boundary than the one
# it watches (guard 27 bans the fs/ubus IMPORT; this bans the CALL, so
# a generator could reach the disk through a helper in homeproxy-pro.uc and
# still pass guard 27)
# (b) the probe reimplemented on isBinary() - which answers "is this text?",
# a question correlated with the format rather than equal to it (see the
# comment on ruleSetFormatFromBytes for why that correlation is luck)
# (c) update_interval passing through raw again, which is the measured
# `time: missing unit in duration "3600"` hard failure
# (d) the form's `format` option growing a default back, which is what made
# "add a local rule-set, pick my .json, forget Format" produce a
# configuration that parsed JSON as a compiled .srs
# (e) the update_interval placeholder showing a unit Go cannot read, which
# is how `1d` got there in the first place
PROBE="$(python3 - "$SCRIPTS" "$VIEWS" <<'PY'
import pathlib, re, sys
scripts, views = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
ruleset_uc = (scripts / 'generator' / 'ruleset.uc').read_text(encoding='utf-8')
homeproxy_uc = (scripts / 'homeproxy-pro.uc').read_text(encoding='utf-8')
client_uc = (scripts / 'generate_client.uc').read_text(encoding='utf-8')
sub_js = (views / 'view' / 'homeproxy-pro' / 'client' / 'subscription.js').read_text(encoding='utf-8')
problems = []
# (a) Only the process boundary may touch the disk for a probe.
for f in sorted((scripts / 'generator').glob('*.uc')):
body = f.read_text(encoding='utf-8')
if re.search(r'\bprobeRuleSetFile\s*\(', re.sub(r'/\*[\s\S]*?\*/', '', body)):
problems.append('%s calls probeRuleSetFile(); only the CLI may read a rule-set file'
% f.name)
if not re.search(r'\bprobeRuleSetFile\s*\(', re.sub(r'/\*[\s\S]*?\*/', '', client_uc)):
problems.append('generate_client.uc no longer probes the rule-set files, so ruleset_formats '
'is never populated and the correction in ruleset.uc can never fire')
# (b) The magic is compared byte by byte, and isBinary() is not the test.
m = re.search(r'export function ruleSetFormatFromBytes\([^)]*\)\s*\{(.*?)\n\};', homeproxy_uc, re.S)
if not m:
problems.append('homeproxy-pro.uc: ruleSetFormatFromBytes() is gone; nothing decides the format')
else:
body = re.sub(r'/\*[\s\S]*?\*/', '', m.group(1))
for byte in ('0x53', '0x52'):
if byte not in body:
problems.append('ruleSetFormatFromBytes() no longer compares the SRS magic byte %s' % byte)
if re.search(r'\bisBinary\s*\(', body):
problems.append('ruleSetFormatFromBytes() calls isBinary(); that answers "is this text?", '
'which is correlated with the rule-set format rather than equal to it')
# (c) update_interval is normalised, not forwarded.
if 'strToTime(cfg.update_interval)' not in ruleset_uc:
problems.append("ruleset.uc no longer runs update_interval through strToTime(); a bare number "
"goes to sing-box as `missing unit in duration`")
if re.search(r'update_interval:\s*cfg\.update_interval\b', ruleset_uc):
problems.append('ruleset.uc forwards update_interval raw again')
# (d)/(e) The rule-set form must not re-introduce the two UI defects.
def option_block(src, name):
m = re.search(r"option\(form\.\w+,\s*'%s'.*?(?=\n\tso = |\n\t/\* Rule set settings end)" % name,
src, re.S)
return m.group(0) if m else None
fmt = option_block(sub_js, 'format')
if fmt is None:
problems.append('subscription.js: the format option is gone')
else:
if re.search(r'\bso\.default\s*=', fmt):
problems.append("the rule-set form gives `format` a default again; an explicit value beats "
"sing-box's extension inference, so every local rule-set ships a format that "
"may contradict its own file")
if not re.search(r'\bso\.rmempty\s*=\s*true', fmt):
problems.append("the rule-set form's `format` is not rmempty; nothing can be left unset, so "
"the generator's read-the-bytes correction is the only thing standing between "
"a user and a rejected configuration")
iv = option_block(sub_js, 'update_interval')
if iv is None:
problems.append('subscription.js: the update_interval option is gone')
else:
if not re.search(r'\bso\.validate\s*=', iv):
problems.append('update_interval has no validator, so a unit Go cannot read reaches sing-box '
'and rejects the whole configuration at apply time')
m = re.search(r"so\.placeholder\s*=\s*'([^']*)'", iv)
if not m:
problems.append('update_interval has no placeholder')
elif not re.fullmatch(r'(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+', m.group(1)):
problems.append("the update_interval placeholder %r is not a Go duration; Go's units are "
"ns/us/ms/s/m/h, and a placeholder is the value users copy verbatim"
% m.group(1))
print('\n'.join(problems))
PY
)" || PROBE="__SCAN_FAILED__"
if [ "$PROBE" = "__SCAN_FAILED__" ]; then
fail "the format-probe scan could not run - fix the guard before trusting a pass"
elif [ -z "$PROBE" ]; then
pass "the format probe reads only at the process boundary, decides on the magic,"
pass " normalises update_interval, and the form neither defaults nor misleads"
else
fail "the format probe / duration normalisation has been undone:"
printf ' %s\n' "$PROBE"
fi
echo
echo "== guard 52: the startup fallback stays opt-in, and the two sides stay in step =="
# ruleset_safe_start points every remote rule-set that has no initial file of
# its own at an EMPTY rule-set, so sing-box starts immediately instead of
# fetching during initialization - before the inbounds bind. On a cold cache
# that is the difference between a router that comes up and one that waits on
# raw.githubusercontent.com, through the node, before it will listen at all.
#
# It is opt-in because the fallback is empty on purpose: a rule-set with no
# rules matches nothing, so everything it would have split falls through to
# `final`. On bypass_mainland_china that means mainland destinations can be
# proxied until the download succeeds. That is a routing change, so the
# default must stay off, exactly as sniffer_advanced_mode's does.
#
# The rest of the guard is about the mechanism being one mechanism: the CLI
# writes the file and the generator points at it, and a disagreement between
# them is silent and fatal (a wrong-format or missing initial file is ignored
# by sing-box, which then blocks startup exactly as if none existed).
SAFE_START="$(python3 - "$ROOT" "$SCRIPTS" <<'PY'
import pathlib, re, sys
root, scripts = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
config_uc = (root / 'root/etc/config/homeproxy-pro').read_text(encoding='utf-8')
context_uc = (scripts / 'generator/context.uc').read_text(encoding='utf-8')
route_uc = (scripts / 'generator/route.uc').read_text(encoding='utf-8')
ruleset_uc = (scripts / 'generator/ruleset.uc').read_text(encoding='utf-8')
common_uc = (scripts / 'generator/common.uc').read_text(encoding='utf-8')
homeproxy_uc = (scripts / 'homeproxy-pro.uc').read_text(encoding='utf-8')
client_uc = (scripts / 'generate_client.uc').read_text(encoding='utf-8')
problems = []
def strip_comments(src):
src = re.sub(r'/\*[\s\S]*?\*/', '', src)
return '\n'.join(l for l in src.split('\n') if not l.lstrip().startswith(('#', '//', '*')))
# (a) The default, in both places it is stated. Same shape as guard 39.
if "ruleset_safe_start: dm.general.ruleset_safe_start || '0'" not in context_uc:
problems.append("context.uc no longer falls back to '0' for ruleset_safe_start; a missing "
"UCI value would have to mean something other than the safe default")
if not re.search(r"^\toption ruleset_safe_start '0'$", config_uc, re.M):
problems.append("the package-shipped /etc/config/homeproxy-pro no longer ships ruleset_safe_start "
"'0'; upgrading users would silently start routing with empty rule-sets")
# (b) An initial_path may only be emitted for a fallback that exists. Both
# sites, because one without the other is the silent failure.
if "ctx.ruleset_initial" not in strip_comments(route_uc):
problems.append('route.uc emits initial_path without checking ctx.ruleset_initial; a built-in '
'could be pointed at a file the CLI never wrote')
if "ctx.ruleset_initial" not in strip_comments(ruleset_uc):
problems.append('ruleset.uc emits initial_path without checking ctx.ruleset_initial')
if 'ruleset_initial' not in client_uc:
problems.append('generate_client.uc no longer writes the fallbacks, so ctx.ruleset_initial is '
'always empty and the opt-in does nothing')
# (c) The built-ins come from the shared list. route.uc hard-coding the tags
# is how the two readers drifted in the first place: the CLI had no idea
# these rule-sets existed.
for tag in ('geoip-cn', 'geosite-cn'):
if re.search(r"tag:\s*'%s'" % re.escape(tag), strip_comments(route_uc)):
problems.append("route.uc hard-codes tag '%s' again; it must come from "
"BUILTIN_REMOTE_RULE_SETS so the CLI can create its fallback" % tag)
if 'BUILTIN_REMOTE_RULE_SETS' not in route_uc:
problems.append('route.uc no longer reads BUILTIN_REMOTE_RULE_SETS')
if 'BUILTIN_REMOTE_RULE_SETS' not in client_uc:
problems.append('generate_client.uc no longer reads BUILTIN_REMOTE_RULE_SETS')
# (d) The shipped constant, byte for byte. It is a binary artifact in the
# tree, and a corrupted one fails in the worst possible way: sing-box
# cannot parse the initial file, ignores it, and blocks startup - the
# exact failure the file exists to prevent. Comparing the bytes here is
# the only thing standing between a bad `cp` and that.
expected_srs = bytes([0x53, 0x52, 0x53, 0x02, 0x78, 0xDA, 0x62, 0x00,
0x0C, 0x00, 0x00, 0x01, 0x00, 0x01])
srs = root / 'root/etc/homeproxy-pro/ruleset/initial/empty.srs'
src = root / 'root/etc/homeproxy-pro/ruleset/initial/empty.source.json'
if not srs.is_file():
problems.append('the offline fallback %s is missing from the package payload' % srs)
elif srs.read_bytes() != expected_srs:
problems.append('%s is %s, not the documented %s; it is the last-resort binary '
'fallback and a corrupted one makes sing-box block startup'
% (srs.name, srs.read_bytes().hex(' '), expected_srs.hex(' ')))
if not src.is_file():
problems.append('the empty source %s is missing from the package payload' % src)
elif src.read_bytes() != b'{"version":3,"rules":[]}':
problems.append('%s is %r, not the documented empty source' % (src.name, src.read_bytes()))
# (e) The fallbacks live inside the rule-set archive, so the existing jail
# mount of HP_DIR and the existing path whitelist both already cover them.
# A directory outside the archive would need a whitelist change and a new
# jail mount, and neither exists.
m = re.search(r"RULESET_INITIAL_DIR\s*=\s*HP_DIR \+ '([^']*)'", homeproxy_uc)
if not m:
problems.append('RULESET_INITIAL_DIR is gone or no longer derived from HP_DIR')
elif not m.group(1).startswith('/ruleset/'):
problems.append('RULESET_INITIAL_DIR is %r, outside the rule-set archive; the jail mount and '
'the path whitelist would not cover it' % m.group(1))
if 'RULESET_INITIAL_DIR' not in common_uc and 'BUILTIN_REMOTE_RULE_SETS' not in common_uc:
problems.append('common.uc no longer carries the shared built-in list')
print('\n'.join(problems))
PY
)" || SAFE_START="__SCAN_FAILED__"
if [ "$SAFE_START" = "__SCAN_FAILED__" ]; then
fail "the safe-start scan could not run - fix the guard before trusting a pass"
elif [ -z "$SAFE_START" ]; then
pass "the startup fallback is opt-in, both sides read one shared list, and the"
pass " shipped constant is byte-for-byte the documented one"
else
fail "the startup fallback has been broken or silently enabled:"
printf ' %s\n' "$SAFE_START"
fi
echo
echo "== guard 53: every dm.general field the generators read is one the Loader lists =="
# config.general in the Loader is an EXPLICIT key list, not a pass-through of
# the UCI section. A field read as `dm.general.foo` that is not in that list
# does not read as "absent" - it reads as "never set", and a `|| '0'` fallback
# in context.uc then makes it indistinguishable from a user who deliberately
# left the option off.
#
# That is not a hypothetical. ruleset_safe_start was added to
# /etc/config/homeproxy-pro and read in context.uc, and nothing said the key was
# missing: the configuration generated cleanly, `sing-box check` passed, every
# test that did not look for the feature's effect was green, and the feature
# was simply never on. Only the e2e case that asserts an initial_path exists
# could see it, and it read as a generator bug.
#
# So this compares the two lists mechanically. The read set is derived from
# what the code actually says, not from a hand-kept list, so a new option
# cannot be added to one side and forgotten on the other.
GENERAL_KEYS="$(python3 - "$SCRIPTS" <<'PY'
import pathlib, re, sys
scripts = pathlib.Path(sys.argv[1])
loader = (scripts / 'config/loader.uc').read_text(encoding='utf-8')
# What the Loader declares on config.general.
m = re.search(r'config\.general\s*=\s*\{(.*?)\n\t\t\};', loader, re.S)
if not m:
print('config/loader.uc: could not find the config.general object literal')
raise SystemExit(0)
declared = set(re.findall(r'^\s*([a-z][a-z0-9_]*)\s*:', m.group(1), re.M))
# What the consumers read off dm.general. Comments stripped first: the prose
# above ruleset_safe_start names dm.general.ruleset_safe_start on purpose, and a
# substring search would read that as a use.
def strip(src):
src = re.sub(r'/\*[\s\S]*?\*/', '', src)
return '\n'.join(l for l in src.split('\n') if not l.lstrip().startswith(('#', '//')))
readers = ['generator/context.uc', 'generate_client.uc']
readers += [str(p.relative_to(scripts)) for p in sorted((scripts / 'generator').glob('*.uc'))]
problems = []
for rel in dict.fromkeys(readers):
f = scripts / rel
if not f.is_file():
continue
src = strip(f.read_text(encoding='utf-8'))
for key in sorted(set(re.findall(r'\bdm\.general\.([a-z][a-z0-9_]*)', src))):
if key not in declared:
problems.append('%s reads dm.general.%s, which the Loader does not declare - '
'the value is always null there, so a `||` fallback makes the '
'option indistinguishable from one the user never set' % (rel, key))
print('\n'.join(problems))
PY
)" || GENERAL_KEYS="__SCAN_FAILED__"
if [ "$GENERAL_KEYS" = "__SCAN_FAILED__" ]; then
fail "the general-key scan could not run - fix the guard before trusting a pass"
elif [ -z "$GENERAL_KEYS" ]; then
pass "every dm.general field the generators and the CLI read is declared by the Loader"
else
fail "the Loader and its consumers disagree about the config section's keys:"
printf ' %s\n' "$GENERAL_KEYS"
fi
echo
echo "== guard 54: the fetch layer uses uclient-fetch, and nothing reintroduces wget =="
# `wget` is whichever implementation the firmware buildroot compiled, and the
# two share almost no options beyond -O. Every fetch in this package used to
# shell out to it with GNU-only flags (-nv --user-agent --timeout= --spider),
# which exits 2 with "unrecognized option" on a busybox-wget router BEFORE any
# request is made - so subscriptions never fetched, resource lists never
# updated, and the connectivity check reported a failed proxy. Reported as
# issue #6.
#
# What let it survive several releases is the interesting part: the suite had a
# guard for the exact shape (the fetch must not fail with a usage error) and it
# only ever ran against the fetcher the host already had - GNU wget on CI, GNU
# wget on the maintainer device. So this guard is not only "no wget" but also
# "and the test that would have caught it runs against a pinned stub".
#
# uclient-fetch is the fetcher OpenWrt itself drives (opkg, sysupgrade, uci) and
# its option set is fixed by the applet rather than by the buildroot, which is
# the only reason one command line can be correct on every target.
FETCH_LAYER="$(python3 - "$ROOT" "$SCRIPTS" "$RPC" <<'PY'
import pathlib
import re
import sys
root, scripts, rpc = (pathlib.Path(p) for p in sys.argv[1:4])
problems = []
def code_only(path):
"""Source with block comments and comment-only lines removed.
Comments are excluded on purpose: several of them explain WHY wget is not
used, and a guard that flagged its own documentation would be deleted
rather than obeyed.
"""
src = path.read_text(encoding="utf-8")
src = re.sub(r"/\*[\s\S]*?\*/", "", src)
lines = [l for l in src.split("\n") if not l.lstrip().startswith(("#", "//"))]
return "\n".join(lines)
# 1. No wget anywhere it could actually be invoked.
wget_call = re.compile(r"(?<![\w/-])wget(?![\w-])")
targets = [p for p in scripts.rglob("*") if p.is_file() and p.suffix in (".uc", ".sh")]
targets.append(rpc)
for path in targets:
for line in code_only(path).split("\n"):
if wget_call.search(line):
problems.append("%s still invokes wget: %s" % (path.name, line.strip()[:90]))
# 2. The fetcher has to be a declared dependency, or a firmware without it in
# base leaves every fetch broken in a much quieter way.
if "+uclient-fetch" not in (root / "Makefile").read_text(encoding="utf-8"):
problems.append("the Makefile does not declare +uclient-fetch; the fetch layer "
"depends on a package it does not require")
# 3. The guard that would have caught this has to run against a stub whose
# accepted option set is pinned, not against the host own fetcher.
runner = (root / "tests/ucode/run.sh").read_text(encoding="utf-8")
stub = root / "tests/fixtures/uclient-fetch-stub"
if "tests/fixtures/uclient-fetch-stub" not in runner:
problems.append("tests/ucode/run.sh no longer points the fetch guard at the pinned stub")
if not stub.is_file():
problems.append("the pinned fetch stub is missing from the package payload")
elif "unrecognized option" not in stub.read_text(encoding="utf-8"):
# A permissive stub cannot catch the regression, which is the whole point:
# it has to reject what it does not recognise, the way the applet does.
problems.append("the fetch stub does not reject unknown options, so it cannot "
"catch an option nobody verified")
print("\n".join(problems))
PY
)" || FETCH_LAYER="__SCAN_FAILED__"
if [ "$FETCH_LAYER" = "__SCAN_FAILED__" ]; then
fail "the fetch-layer scan could not run - fix the guard before trusting a pass"
elif [ -z "$FETCH_LAYER" ]; then
pass "no wget in the fetch layer, uclient-fetch is a declared dependency, and the"
pass " guard that catches a bad option runs against a pinned stub"
else
fail "the fetch layer is not pinned to one fetcher:"
printf ' %s\n' "$FETCH_LAYER"
fi
echo
printf '%s checks, %s failures\n' "$checks" "$([ "$FAILED" = 0 ] && echo 0 || echo 'nonzero')"
if [ "$FAILED" != 0 ]; then
@@ -0,0 +1,94 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Regression fixture for config.ruleset_safe_start = '1' (issue #7 batch 2a).
#
# custom.uci with the opt-in on and two REMOTE rule-sets added, because those
# are the only ones the fallback applies to: a local rule-set already has its
# file on disk and is never fetched during initialization.
#
# It is a separate file rather than three sed variations of custom.uci for two
# reasons. The opt-in has to be in the `config homeproxy-pro 'routing'` section
# (dm.general.ruleset_safe_start), which is a different section from anything
# the variations were anchored on; and a multi-expression sed that both inserts
# an option and appends a section is a construct this suite had never run -
# the one-expression form is proven, the two-expression form is not, and a test
# whose own tooling is untested is how a case silently stops testing anything.
#
# The two rule-sets cover the two shapes the fallback has to get right:
#
# rs_remote one tag. initial_path is a concrete path and one file is
# written for it.
# rs_multi two tags. sing-box substitutes {tag} and opens EVERY resulting
# file, so initial_path must carry the placeholder and one file
# must be written per tag - a rule-set with a literal
# "{tag}.srs" on disk falls back to downloading and blocks
# startup, which is the exact failure the feature removes.
#
# __RULESET_DIR__ is substituted by tests/ucode/test_generators.sh with the
# per-run archive.
config homeproxy-pro 'config'
option routing_mode 'custom'
option proxy_mode 'redirect_tproxy'
option ipv6_support '0'
option log_level 'error'
option ruleset_safe_start '1'
config homeproxy-pro 'routing'
option default_outbound 'direct-out'
option default_outbound_dns 'default-dns'
config homeproxy-pro 'infra'
option mixed_port '5330'
option dns_port '5333'
option redirect_port '5331'
option self_mark '100'
# direct node: exercises the direct_overrides write path
config node 'n_direct'
option label 'direct-node'
option type 'direct'
option override_address '1.2.3.4'
option override_port '8443'
# routing node + rule: exercises get_direct_override() reading direct_overrides
config routing_node 'rn_direct'
option enabled '1'
option label 'rn-direct'
option node 'n_direct'
option domain_resolver 'default-dns'
option domain_strategy 'prefer_ipv4'
config routing_rule 'rr_direct'
option enabled '1'
option label 'rr-direct'
option outbound 'rn_direct'
option action 'route'
# The two remote rule-sets the fallback applies to. Neither has an
# initial_path of its own, which is the whole precondition: a rule-set that has
# one is left completely alone by the fallback.
config ruleset 'rs_remote'
option enabled '1'
option label 'remote-rs'
option type 'remote'
option format 'binary'
option url 'https://example.invalid/remote.srs'
config ruleset 'rs_multi'
option enabled '1'
option label 'multi-rs'
option type 'remote'
option format 'binary'
option url 'https://example.invalid/{tag}.srs'
list extra_tags 'alt'
# A disabled remote rule-set: the fallback must not create a file for it, since
# build_user_rulesets() skips it before it can ever reach the generated
# configuration.
config ruleset 'rs_off'
option enabled '0'
option label 'off-rs'
option type 'remote'
option format 'binary'
option url 'https://example.invalid/off.srs'
+93
View File
@@ -0,0 +1,93 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# A stand-in for uclient-fetch that accepts ONLY the options the package is
# allowed to use, and rejects everything else the way the real applet does.
#
# WHY THIS EXISTS
#
# The subscription fetch used to shell out to `wget` with GNU-only options
# (-nv --user-agent --timeout=). `wget` is whichever implementation the
# firmware's buildroot compiled, and the two share almost no options, so on a
# busybox-wget router wget exited 2 with "unrecognized option: n" BEFORE making
# a single request: every subscription fetch failed, every resource-list update
# failed, and the connectivity check reported a failed proxy.
#
# The suite already had a guard for exactly this shape ("the fetch must not
# fail with a usage error") and it still did not catch it, because it ran
# against whatever fetcher the host happened to have - GNU wget on CI, GNU
# wget on the maintainer's own router. The guard was pointed at the
# configuration that already worked.
#
# This stub is what makes the guard mean something: the accepted option set is
# pinned to the list read off the applet on a real device, so an option nobody
# verified is rejected here the same way it would be rejected there, on any
# host, with no network.
#
# -O <file> stdout is "-"
# -U <str> | --user-agent <str>
# -T N | --timeout=N seconds
# -s | --spider existence check only
# -q | --quiet
# -4 | -6
# --header=K: V note the '=': the space-separated form is NOT accepted
#
# Behaviour that matters to the callers, copied from the real applet measured
# on 192.168.1.1 (sing-box 1.14.2, ImmortalWrt 25.12.2):
#
# * the requested URL IS echoed to stderr, query string and all, so the
# redaction in wGETVerbose() is exercised rather than assumed;
# * a connect failure is exit 4 and "Failed to send request: …";
# * an HTTP error is a non-zero exit and "HTTP error <code>";
# * a success is exit 0 with the body on stdout.
url=""
out=""
spider=0
quiet=0
while [ $# -gt 0 ]; do
case "$1" in
-O) out="$2"; shift 2 ;;
-O?*) out="${1#-O}"; shift ;;
-U|--user-agent) shift 2 ;;
--user-agent=*) shift ;;
-T) shift 2 ;;
--timeout=*) shift ;;
-s|--spider) spider=1; shift ;;
-q|--quiet) quiet=1; shift ;;
-4|-6) shift ;;
--header=*) shift ;;
-*) # anything else is unverified: reject it, exactly as
# the applet does, so the guard can see it.
printf 'uclient-fetch: unrecognized option: %s\n' "$(printf '%s' "$1" | sed 's/^--*//')" >&2
printf 'Usage: uclient-fetch [options] <URL>\nOptions: -4 -6 -O <file> --user-agent -T N --spider -q --header=...\n' >&2
exit 1 ;;
*) url="$1"; shift ;;
esac
done
[ -n "$url" ] || { printf 'uclient-fetch: no URL\n' >&2; exit 1; }
say() { [ "$quiet" = 1 ] || printf '%s\n' "$*" >&2; }
# The real applet announces the target on stderr before it reports anything
# else. This is what puts a subscription token into the error text, and it is
# why the redaction assertions below are worth anything.
say "Downloading '$url'"
case "$url" in
*HP_T_STUB_HTTP_ERROR*)
say "HTTP error 404"
exit 8
;;
*HP_T_STUB_BINARY*)
# A 200 with a binary body. This is the shape that used to be reported
# as "fetch failed: … Download completed (34185 bytes)".
say "Download completed (14 bytes)"
printf 'SRS\002x\332b\000\014\000\000\001\000\001'
exit 0
;;
esac
say "Failed to send request: Operation not permitted"
exit 4
@@ -176,5 +176,95 @@ function makePoll() {
check('the registered handler calls read() when invoked', reads === 1, `${reads} reads`);
}
/* --- the rule-set degradation report ------------------------------------
*
* parseRuleSetFetchFailures() is the only thing that tells a user a rule-set
* is currently EMPTY, because with ruleset_safe_start on there is no other
* symptom: the instance is healthy, the health gate passes (nothing in
* health.sh looks at rule-sets), and the LAN is proxied.
*
* It parses a file sing-box is concurrently writing and that clean_log.sh
* truncates at 50 KB, so the cases below are about tolerance and about not
* lying: a missing timestamp must read as "no timestamp", never as a guess.
*/
const FAILURES = hp.parseRuleSetFetchFailures;
/* The first two lines are VERBATIM from a real sing-box 1.14.2 run on the
* target (an isolated instance with the rule-set URLs pointed at an
* unreachable port), including the `+0800` UTC offset that precedes the date.
*
* That offset is the reason this case exists. The first version of the
* timestamp pattern was anchored at the start of the line and matched a bare
* date, which parses fine against every hand-written sample and returns null
* for every line the device actually writes - so the panel would have shown
* the right tag and the right reason and no time on every real failure. Only
* a real line catches that, and it is pinned here so it cannot come back. */
const SAMPLE = [
'+0800 2026-10-03 09:29:34 INFO sing-box started (0.05s)',
'+0800 2026-10-03 09:29:34 ERROR router: fetch rule-set geoip-cn: Get "http://127.0.0.1:9/geoip-cn.srs": remote error: open connection to 127.0.0.1:9 using outbound/direct[direct]: dial tcp 127.0.0.1:9: connect: connection refused',
'+0800 2026-10-03 09:29:34 ERROR router: fetch rule-set geosite-cn: Get "http://127.0.0.1:9/geosite-cn.srs": remote error: open connection to 127.0.0.1:9 using outbound/direct[direct]: dial tcp 127.0.0.1:9: connect: connection refused',
''
].join('\n');
check('an empty log yields no failures', FAILURES('').length === 0);
check('a log of blank lines yields no failures', FAILURES('\n\n').length === 0);
check('a log with no fetch lines yields no failures',
FAILURES('2026-10-03 01:15:42 INFO router: loaded rule-set geoip-cn').length === 0);
const sample = FAILURES(SAMPLE);
check('both failing tags are reported', sample.length === 2, JSON.stringify(sample));
check('the tag is captured without the surrounding prose',
sample.map((f) => f.tag).join(',') === 'geoip-cn,geosite-cn',
JSON.stringify(sample.map((f) => f.tag)));
check('the reason is captured',
sample[0].reason.startsWith('Get "http://'), JSON.stringify(sample[0].reason));
check('the timestamp is captured despite the +0800 offset before it',
sample[0].at === '2026-10-03 09:29:34', String(sample[0].at));
check('the reason survives the full real line',
sample[1].reason.startsWith('Get "http://127.0.0.1:9/'), JSON.stringify(sample[1].reason));
check('the reason keeps the nested colons and quotes',
sample[1].reason.includes('dial tcp 127.0.0.1:9: connect: connection refused'),
JSON.stringify(sample[1].reason));
/* A negative UTC offset, and a build that omits the offset entirely, both have
* to keep working - the offset is optional in the pattern on purpose. */
const westOfUTC = FAILURES('-0500 2026-12-31 20:00:00 ERROR router: fetch rule-set geoip-cn: timeout');
check('a negative UTC offset still yields the time',
westOfUTC[0].at === '2026-12-31 20:00:00', String(westOfUTC[0].at));
const noOffset = FAILURES('2026-12-31 20:00:00 ERROR router: fetch rule-set geoip-cn: timeout');
check('a line with no offset at all still yields the time',
noOffset[0].at === '2026-12-31 20:00:00', String(noOffset[0].at));
/* A rule-set that fails on every 24-hour refresh is in the log many times, and
* the file is truncated - so the most recent line has to win, or the panel
* shows a reason from yesterday as if it were current. */
const REPEATED = [
'+0800 2026-10-01 01:00:00 ERROR router: fetch rule-set geoip-cn: old failure',
'+0800 2026-10-03 01:00:00 ERROR router: fetch rule-set geoip-cn: new failure'
].join('\n');
const repeated = FAILURES(REPEATED);
check('a repeated tag is reported once', repeated.length === 1, JSON.stringify(repeated));
check('the most recent failure wins for a repeated tag',
repeated[0].reason === 'new failure', JSON.stringify(repeated[0].reason));
/* clean_log.sh can cut mid-line, and a truncated line must not become a
* failure with a nonsense reason - but it must still name its tag, or the
* panel would hide a rule-set that really is failing. */
const truncated = FAILURES('+0800 2026-10-03 01:00:00 ERROR router: fetch rule-set geoip-cn: Get "https://exa');
check('a truncated line still names its tag', truncated.length === 1, JSON.stringify(truncated));
check('a truncated line keeps the timestamp it has',
truncated[0].at === '2026-10-03 01:00:00', String(truncated[0].at));
/* The format is not assumed. A line with no timestamp is still reported, with
* the time simply absent - the panel omits it rather than inventing one. */
const noTime = FAILURES('ERROR router: fetch rule-set geosite-cn: dial tcp: i/o timeout');
check('a line without a timestamp is still reported', noTime.length === 1, JSON.stringify(noTime));
check('a line without a timestamp reports no time', noTime[0].at === null, String(noTime[0].at));
check('a line without a timestamp still yields the reason',
noTime[0].reason === 'dial tcp: i/o timeout', JSON.stringify(noTime[0].reason));
check('the parser tolerates null', FAILURES(null).length === 0);
check('the parser tolerates undefined', FAILURES(undefined).length === 0);
console.log(`frontend rpc fallbacks: ${checks} checks, ${failures} failures`);
process.exit(failures ? 1 : 0);
@@ -133,6 +133,45 @@ check('cert path ignores an empty value',
check('cert path ignores a missing section',
hp.validateCertificatePath(null, '/etc/passwd') === true);
/* validateRuleSetPath: the rule-set policy has to be the same list the backend
* enforces (RULE_PATH_ROOTS in homeproxy-pro.uc; guard 50 compares them
* textually). It is deliberately NARROWER than the certificate list and than
* the backend's general HP_DIR gate - a rule-set belongs in the archive and
* nowhere else - so the interesting assertions are the ones where a path is
* inside /etc/homeproxy-pro but outside the archive, and where it is under the
* /tmp/homeproxy_ prefix the certificate list also refuses. A UI built on the
* general gate would offer both, and the generator would then refuse them. */
check('rule-set path accepts the archive',
hp.validateRuleSetPath('sec1', '/etc/homeproxy-pro/ruleset/example.srs') === true);
check('rule-set path rejects a bare root',
isError(hp.validateRuleSetPath('sec1', '/etc/homeproxy-pro/ruleset/')));
check('rule-set path rejects /etc/passwd',
isError(hp.validateRuleSetPath('sec1', '/etc/passwd')));
check('rule-set path rejects another path under /etc/homeproxy-pro',
isError(hp.validateRuleSetPath('sec1', '/etc/homeproxy-pro/resources/china_ip4.json')));
check('rule-set path rejects the certs directory',
isError(hp.validateRuleSetPath('sec1', '/etc/homeproxy-pro/certs/server_privatekey.pem')));
check('rule-set path rejects /tmp/homeproxy_ upload staging',
isError(hp.validateRuleSetPath('sec1', '/tmp/homeproxy_ruleset_upload.tmp')));
check('rule-set path rejects a traversal out of the archive',
isError(hp.validateRuleSetPath('sec1', '/etc/homeproxy-pro/ruleset/../../etc/shadow')));
check('rule-set path rejects a relative path',
isError(hp.validateRuleSetPath('sec1', 'etc/homeproxy-pro/ruleset/x.srs')));
check('rule-set path ignores an empty value',
hp.validateRuleSetPath('sec1', '') === true);
check('rule-set path ignores a missing section',
hp.validateRuleSetPath(null, '/etc/passwd') === true);
/* The placeholder and the datalist entry the form offers have to be paths this
* validator accepts. A placeholder naming a directory the field then refuses is
* the same class of bug the mirrored lists exist to prevent, and the
* placeholder is what a user copies. */
const defaultPath = hp.rule_path_default;
check('the rule-set form offers a default path',
typeof defaultPath === 'string' && defaultPath.length > 0, String(defaultPath));
check('the offered default path passes its own validator',
hp.validateRuleSetPath('sec1', defaultPath) === true, String(defaultPath));
/* --- dns_server: the legacy 'wan' value has to stay acceptable ---------- *
* The preset-mode "Overseas DNS server" field used to offer a 'wan' entry
* ("WAN DNS (read from interface)"). The entry is gone from the dropdown -
@@ -228,5 +267,92 @@ check('dns_server still rejects an empty value', isError(validateDnsServer('')))
check('dns_server still rejects a value that is neither a hostname nor an address',
isError(validateDnsServer('not a dns server')));
/* --- subscription.js: the update_interval validator ---------------------- *
* sing-box parses update_interval as a Go duration. A bare number used to
* reach it unchanged and come back as a measured hard failure - `time:
* missing unit in duration "3600"` - which rejects the WHOLE configuration,
* so the reload is aborted and the user sees only "my change did not take".
* The generator now normalises bare numbers (strToTime), and this validator
* is what stops the shapes Go cannot read at all, on the page.
*
* The callback is registered inline in render(), so the module is loaded
* against a form mock and the stored option is driven the way a widget calls
* it. `1d` is the important rejection: it is what the placeholder used to
* show, it is the value a user is most likely to copy, and Go's duration
* units are ns/us/ms/s/m/h - there is no day.
*/
function updateIntervalValidate() {
const classes = [ 'Value', 'ListValue', 'Flag', 'DynamicList', 'MultiValue', 'TextValue',
'Button', 'TypedSection', 'NamedSection', 'GridSection', 'SectionValue' ];
const formMock = {};
for (const name of classes) {
formMock[name] = class {};
formMock[name].__name__ = name;
}
let captured = null;
const sectionMock = {
tab() {},
taboption(_tab, kind, name, title, description) { return this.option(kind, name, title, description); },
option(kind, name, title, description) {
const option = { kind, name, title, description, value() {}, depends() {} };
if (kind && kind.__name__ === 'SectionValue')
option.subsection = sectionMock;
if (name === 'update_interval')
captured = option;
return option;
}
};
const sub = loadLuciModule(
path.join(root, 'htdocs/luci-static/resources/view/homeproxy-pro/client/subscription.js'),
{
baseclass: { extend: (o) => o },
form: formMock,
uci: { sections: () => {} },
homeproxy-pro: {
renderSectionAdd: () => {},
loadModalTitle: () => {},
loadDefaultLabel: () => {},
validateUniqueValue: () => true,
validateRuleSetPath: () => true,
rule_path_default: '/etc/homeproxy-pro/ruleset/example.srs'
}
},
/* The module binds four of its labels through L.bind() at render
* time, so the runtime has to carry one. A passthrough is enough -
* nothing under test reads what those labels resolve to - but the
* default runtime has no bind() at all, which is why this mock is
* passed rather than left implicit. */
{ L: { bind: (fn) => fn } }
);
sub.render({ s: sectionMock });
const validator = captured && captured.validate;
if (typeof validator !== 'function')
throw new Error('subscription.js no longer registers an update_interval validator');
const ctx = { section: { formvalue: () => 'remote' } };
return (value) => validator.call(ctx, 'ruleset0', value);
}
const validateUpdateInterval = updateIntervalValidate();
/* Accepted: a bare number (the generator appends "s") and Go's own duration
* grammar, including compounds like 1h30m. */
for (const ok of [ '3600', '24h', '1h', '30m', '1h30m', '500ms', '1.5h', '90s' ])
check(`update_interval accepts '${ok}'`, validateUpdateInterval(ok) === true, String(validateUpdateInterval(ok)));
/* Refused: a day/week/year unit, prose, a spaced value, and a negative. */
for (const bad of [ '1d', '1w', '1y', 'daily', '24 h', '-1h', '1 h', 'h', '1x' ])
check(`update_interval rejects '${bad}'`, isError(validateUpdateInterval(bad)));
/* Optional, not required: an empty value means "no update_interval", which
* sing-box reads as its own default. Making the field required would refuse
* to save an existing, working remote rule-set. */
check('update_interval accepts an empty value', validateUpdateInterval('') === true);
check('update_interval ignores a missing section', validateUpdateInterval(null, '1d') === true);
console.log(`frontend validators: ${checks} checks, ${failures} failures`);
process.exit(failures ? 1 : 0);
+1 -1
View File
@@ -161,7 +161,7 @@ echo "== crontab permissions =="
sh "$ROOT/tests/runtime/test_crontab_perms.sh" "$ROOT" "$WORK_ROOT/crontab-perms" || FAILED=1
echo "== resource update verification (B2) =="
# Pure shell: wget / jsonfilter / ucode / uci / flock are stubbed and the
# Pure shell: uclient-fetch / jsonfilter / ucode / uci / flock are stubbed and the
# script's absolute paths are rewritten into a sandbox, so this drives the
# verification path - match, mismatch, no API digest, no local digest - without
# a network or a router. What the *shipped* digest helper computes is
@@ -15,7 +15,7 @@
# 4. no local digest -> NOT installed (fail closed)
# plus: already up to date -> no download at all
#
# Everything external is stubbed (wget, jsonfilter, ucode, uci, flock) and the
# Everything external is stubbed (uclient-fetch, jsonfilter, ucode, uci, flock) and the
# script's absolute paths are rewritten into a sandbox, so this is pure shell
# and runs on a laptop, in CI and on a target. The ucode stub answers with a
# fixed digest: what the *shipped* helper computes is
@@ -65,27 +65,49 @@ for anchor in "RESOURCES_DIR=\"$WORK/resources\"" "RUN_DIR=\"$WORK/run\""; do
done
# --- stubs ------------------------------------------------------------------
# wget serves three shapes, chosen by the URL: the commit list, the contents
# metadata, and the file itself. What the contents API reports and what the
# file contains are control files, so each case can make them agree or not.
cat > "$WORK/bin/wget" <<'EOF'
# uclient-fetch serves three shapes, chosen by the URL: the commit list, the
# contents metadata, and the file itself. What the contents API reports and what
# the file contains are control files, so each case can make them agree or not.
#
# It replaced a wget stub, and the option handling is the point rather than an
# afterthought: this is the only thing that would have noticed the real
# regression, where the script passed GNU-only flags (--timeout= --spider)
# that a busybox-wget target rejects outright. A permissive stub ("ignore what
# you do not recognise") cannot catch that, so unknown options are a hard
# failure here - the same way the applet behaves.
cat > "$WORK/bin/uclient-fetch" <<'EOF'
#!/bin/sh
url=""
out=""
while [ $# -gt 0 ]; do
case "$1" in
-O) out="$2"; shift 2 ;;
-O-|--spider) shift ;;
--timeout=*|--header) shift 2 ;;
-O*) out="${1#-O}"; shift ;;
--user-agent=*) shift ;;
--header=*) shift ;;
--timeout=*) shift ;;
-T) shift 2 ;;
-s|--spider|-q|--quiet|-4|-6) shift ;;
-*) printf 'uclient-fetch: unrecognized option: %s\n' "${1#-}" >&2
printf 'Usage: uclient-fetch [options] <URL>\n' >&2
exit 1 ;;
*) url="$1"; shift ;;
esac
done
emit() { [ -n "$out" ] && printf '%s' "$1" > "$out" || printf '%s' "$1"; }
# "-" means stdout, which is the applet's own convention - getting that wrong
# writes the body to a file named "-" and leaves the parser reading nothing.
emit() {
if [ -n "$out" ] && [ "$out" != "-" ]; then
printf '%s' "$1" > "$out"
else
printf '%s' "$1"
fi
}
case "$url" in
*api.github.com/*/commits*) emit "$(cat "$HP_T_API_COMMITS")" ;;
*api.github.com/*/contents*) emit "$(cat "$HP_T_API_CONTENTS")" ;;
*)
if [ -n "${HP_T_WGET_FAIL:-}" ]; then exit 1; fi
if [ -n "${HP_T_FETCH_FAIL:-}" ]; then exit 1; fi
emit "$(cat "$HP_T_BODY")"
;;
esac
@@ -3146,7 +3146,7 @@
"kind": "ListValue",
"name": "format",
"title": "Format",
"description": null,
"description": "Rule-set file format. Leave it empty and HomeProxy reads it from the file itself; a value that does not match the file is corrected and reported in the log.",
"depends": [],
"values": [
[
@@ -3160,28 +3160,33 @@
],
"subsection": null,
"props": {
"default": "binary",
"rmempty": false
"rmempty": true
}
},
{
"kind": "Value",
"name": "path",
"title": "Path",
"description": null,
"description": "Rule-set file in /etc/homeproxy-pro/ruleset/. Copy it there first: the generator refuses a rule-set whose file is missing.",
"depends": [
[
"type",
"local"
]
],
"values": [],
"values": [
[
"/etc/homeproxy-pro/ruleset/example.srs",
null
]
],
"subsection": null,
"props": {
"datatype": "file",
"modalonly": true,
"placeholder": "/etc/homeproxy-pro/ruleset/example.json",
"rmempty": false
"placeholder": "/etc/homeproxy-pro/ruleset/example.srs",
"rmempty": false,
"validate": "fn(279:52e3f36f3a3c)"
}
},
{
@@ -3224,18 +3229,25 @@
"kind": "Value",
"name": "initial_path",
"title": "Initial path",
"description": "Local file with initial rule-set content; avoids blocking startup on first download (1.14).",
"description": "Local file with initial rule-set content; avoids blocking startup on first download (1.14). Must be under /etc/homeproxy-pro/ruleset/.",
"depends": [
[
"type",
"remote"
]
],
"values": [],
"values": [
[
"/etc/homeproxy-pro/ruleset/example.srs",
null
]
],
"subsection": null,
"props": {
"datatype": "file",
"modalonly": true
"modalonly": true,
"placeholder": "/etc/homeproxy-pro/ruleset/example.srs",
"validate": "fn(279:52e3f36f3a3c)"
}
},
{
@@ -3260,7 +3272,7 @@
"kind": "Value",
"name": "update_interval",
"title": "Update interval",
"description": "Update interval of rule set.",
"description": "Update interval of rule set, e.g. 24h or 3600 (seconds).",
"depends": [
[
"type",
@@ -3270,7 +3282,8 @@
"values": [],
"subsection": null,
"props": {
"placeholder": "1d"
"placeholder": "24h",
"validate": "fn(242:e40abf36902a)"
}
}
],
@@ -4,7 +4,7 @@
* homeproxy-pro module shim for subscription/fetcher unit tests.
*
* The real homeproxy-pro.uc wraps wGETVerbose around `executeCommand(
* /usr/bin/wget ...)`, which means a unit test that imports it
* uclient-fetch ...)`, which means a unit test that imports it
* would shell out to the network. Tests stage this file as
* `homeproxy-pro` in their -L directory so the fetcher picks up the
* shim's wGETVerbose instead.
+18 -1
View File
@@ -379,7 +379,24 @@ rm -rf "$WORK/homeproxy-pro"
mkdir -p "$WORK/homeproxy-pro"
cp "$ROOT/root/etc/homeproxy-pro/scripts/homeproxy-pro.uc" "$WORK/homeproxy-pro/"
cp "$ROOT/tests/ucode/test_homeproxy_utils.uc" "$WORK/homeproxy-pro/"
if ( cd "$WORK/homeproxy-pro" && ucode test_homeproxy_utils.uc ); then
# The fetch-layer guard runs against a stub, not against whatever fetcher this
# host happens to have. That is the whole point: the guard exists to catch
# "an option the fetcher does not have", and while it ran against the host's
# own binary it only ever checked the configuration that already worked - CI's
# GNU wget, the maintainer's GNU wget. The stub accepts exactly the option list
# read off the applet on a device and rejects the rest, so the check means the
# same thing everywhere and needs no network.
#
# On a target /bin/uclient-fetch really exists, so fetchBinary() returns it and
# the same assertions run against the real applet. Both directions are useful:
# the stub catches "an option we invented", the target catches "an option the
# real applet lacks".
mkdir -p "$WORK/fetchbin"
cp "$ROOT/tests/fixtures/uclient-fetch-stub" "$WORK/fetchbin/uclient-fetch"
chmod +x "$WORK/fetchbin/uclient-fetch"
if ( cd "$WORK/homeproxy-pro" && PATH="$WORK/fetchbin:$PATH" ucode test_homeproxy_utils.uc ); then
echo "PASS: executeCommand() regression tests"
else
echo "FAIL: executeCommand() regression tests"
@@ -23,15 +23,6 @@ WORK="${2:-/tmp/hp-generator-test}"
ROOT="$(cd "$ROOT" && pwd)"
FAILED=0
# The local rule-set fixture has to live under /tmp/homeproxy_ (see below), so
# it cannot sit inside $WORK; this is the per-run root that holds it instead.
# Created lazily by the first case that needs one.
RULESET_ROOT=""
cleanup() {
[ -n "$RULESET_ROOT" ] && rm -rf "$RULESET_ROOT"
}
trap cleanup EXIT INT TERM
run_case() {
name="$1"
fixture="$2"
@@ -83,35 +74,41 @@ run_case() {
fi
if grep -q "__RULESET_DIR__" "$fixture"; then
# The fixture needs a real local rule-set on disk. The path must
# live under /tmp/homeproxy_* (validateHomeProxyPath() in
# homeproxy-pro.uc whitelists /etc/homeproxy-pro/ and /tmp/homeproxy_
# only, and the custom fixture exercises the local-rule-set
# path whitelist gate introduced by the security patch).
# The fixture needs a real local rule-set on disk. The path has to
# satisfy validateRuleSetPath(), whose RULE_PATH_ROOTS was rewritten
# to this run's scratch tree when homeproxy-pro.uc was staged below - so
# the archive IS $dir/ruleset and no out-of-tree copy is needed.
#
# It used to be copied out to a /tmp/homeproxy_* directory instead,
# because the whitelist then admitted /tmp/homeproxy_*. Narrowing the
# policy to the single archive root removed the reason for the copy
# and, with it, the per-run mktemp that made this the only case in
# the suite reaching outside its own work dir.
printf '%s' '{"version":1,"rules":[{"domain_suffix":["example.com"]}]}' > "$dir/ruleset/src.json"
if ! sing-box rule-set compile "$dir/ruleset/src.json" -o "$dir/ruleset/test.srs"; then
echo "FAIL: $name: could not compile the local rule-set fixture"
FAILED=1
return
fi
# Stage the ruleset under a /tmp/homeproxy_* directory so the
# whitelist recognises the staging path. Production paths
# typically live at /etc/homeproxy-pro/ruleset/...
# Two more files in the archive, for the format-probe cases. Both
# hold a VALID version-3 source rule-set; they differ only in what
# they are called, which is the whole point:
#
# The path cannot move under $WORK (validateHomeProxyPath() in
# homeproxy-pro.uc whitelists /etc/homeproxy-pro/ and /tmp/homeproxy_
# only), but it can still be per-run: mktemp gives each run its
# own tree, and the trap removes it even when a case bails out
# early. The old fixed /tmp/homeproxy_test_ruleset/$name leaked
# one directory per run and let two concurrent runs overwrite
# each other's compiled .srs.
if [ -z "$RULESET_ROOT" ]; then
RULESET_ROOT="$(mktemp -d /tmp/homeproxy_test_ruleset.XXXXXX)"
fi
HP_RULESET="$RULESET_ROOT/$name"
mkdir -p "$HP_RULESET"
cp "$dir/ruleset/test.srs" "$HP_RULESET/test.srs"
sed "s#__RULESET_DIR__#$HP_RULESET#" "$fixture" > "$dir/config/homeproxy-pro"
# source.json the name agrees with the content - the baseline
# a correct configuration already looks like
# mislabelled.srs source content under a .srs name, i.e. exactly
# the state sing-box's extension inference gets
# wrong ("invalid sing-box rule-set file")
#
# They cannot be the same file: the cases need the *name* to be the
# variable while the content stays valid, and a rule-set that failed
# `sing-box check` for its own reasons would make every assertion
# about the correction meaningless.
printf '%s' '{"version":3,"rules":[{"domain_keyword":["example.com"]}]}' > "$dir/ruleset/source.json"
cp "$dir/ruleset/source.json" "$dir/ruleset/mislabelled.srs"
sed "s#__RULESET_DIR__#$dir/ruleset#" "$fixture" > "$dir/config/homeproxy-pro"
else
cp "$fixture" "$dir/config/homeproxy-pro"
fi
@@ -130,6 +127,16 @@ run_case() {
fi
# Stage the two empty-rule-set files the "do not block startup" fallback
# uses. RULESET_INITIAL_DIR is derived from HP_DIR, which was rewritten
# to $dir above, so the generator looks for them under $dir/ruleset/initial
# and the repository's copy has to be staged there. Copied rather than
# referenced so a case that deletes or corrupts one cannot damage the
# checkout - the byte-exactness of the constant is arch-guard 52's job.
mkdir -p "$dir/ruleset/initial"
cp "$ROOT/root/etc/homeproxy-pro/ruleset/initial/empty.srs" "$dir/ruleset/initial/"
cp "$ROOT/root/etc/homeproxy-pro/ruleset/initial/empty.source.json" "$dir/ruleset/initial/"
# HP_VALIDATE_DATA lets a development host replace /sbin/validate_data
# (see tests/README.md); on a target the production path is kept.
#
@@ -139,13 +146,33 @@ run_case() {
# the staging seam out of the source files - staging it as
# HP_DIR + '/config' instead would silently diverge from production,
# which is exactly the bug this constant replaced.
#
# RULE_PATH_ROOTS is rewritten for the same reason and with the same
# intent: it is the archive the local rule-set fixture has to live in,
# and the suite must not have to write to the real /etc/homeproxy-pro/ruleset
# to exercise the whitelist. The value is this run's own $dir/ruleset -
# the same tree __RULESET_DIR__ was substituted with above.
VALIDATE_DATA="${HP_VALIDATE_DATA:-/sbin/validate_data}"
sed -e "s#^export const HP_DIR = '/etc/homeproxy-pro';#export const HP_DIR = '$dir';#" \
-e "s#^export const RUN_DIR = '/var/run/homeproxy-pro';#export const RUN_DIR = '$dir/run';#" \
-e "s#^export const UCICONFIG_DIR = '/etc/config';#export const UCICONFIG_DIR = '$dir/config';#" \
-e "s#^export const RULE_PATH_ROOTS = \\['/etc/homeproxy-pro/ruleset/'\\];#export const RULE_PATH_ROOTS = ['$dir/ruleset/'];#" \
-e "s#/sbin/validate_data#${VALIDATE_DATA}#" \
"$ROOT/root/etc/homeproxy-pro/scripts/homeproxy-pro.uc" > "$dir/scripts/homeproxy-pro.uc"
# The rewrite above is a staging seam, so it has to be verified rather
# than assumed: a sed that stopped matching would leave the production
# roots in place, every local rule-set case would be refused by the
# whitelist, and the suite would report a generator regression instead.
# -F, because the bracket expressions would otherwise have to be escaped
# and this script also runs under busybox grep on a target.
if grep -qF "export const RULE_PATH_ROOTS = ['/etc/homeproxy-pro/ruleset/']" \
"$dir/scripts/homeproxy-pro.uc"; then
echo "FAIL: $name: RULE_PATH_ROOTS was not rewritten into the sandbox"
FAILED=1
return
fi
# Stage the config/ subtree (Loader / Model / Adapter, imported via
# the relative path "../config/*.uc" by the generator modules).
cp "$ROOT/root/etc/homeproxy-pro/scripts/config/loader.uc" "$dir/scripts/config/"
@@ -1171,17 +1198,334 @@ else
echo "PASS: cache-file-custom: experimental.cache_file emitted for routing_mode='custom'"
fi
# 5) P3 #7: a local ruleset whose path is outside the homeproxy-pro
# whitelist (e.g. /etc/passwd) used to silently set the field to
# null. The fix is to die() with a message naming the offender.
# run_case_type_error observes the refusal. The variation targets
# the *staged* config (after run_case substituted the placeholder
# with the per-run /tmp path), so the pattern is on the real path
# prefix rather than the fixture literal.
run_case_type_error local-ruleset-bad-path "outside the homeproxy-pro whitelist" \
# 5) P3 #7: a local ruleset whose path is outside the allowed rule-set roots
# used to be silently dropped (and later, for initial_path, silently set to
# null). The fix is to die() with a message naming the offender.
# run_case_type_error observes the refusal. The variation targets the
# *staged* config, whose path was rewritten by run_case to this run's
# scratch archive, so the pattern is on the staged path rather than the
# fixture's __RULESET_DIR__ literal.
#
# The roots were narrowed to the single archive (RULE_PATH_ROOTS), so this now
# also pins that a path inside /etc/homeproxy-pro but OUTSIDE the archive is
# refused - which the old /etc/homeproxy-pro/-wide gate would have accepted.
run_case_type_error local-ruleset-bad-path "outside the allowed rule-set roots" \
"$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json \
"s%^[[:space:]]*option path '/tmp/homeproxy_test_ruleset\\.[^/]*/local-ruleset-bad-path/test.srs'% option path '/etc/passwd'%"
"s%^[[:space:]]*option path '.*/ruleset/test.srs'% option path '/etc/homeproxy-pro/resources/test.srs'%"
# 5b) The traversal form of the same hole. A prefix comparison alone accepts
# /etc/homeproxy-pro/ruleset/../../etc/shadow, and sing-box opens the rule-set
# as root, so the gate has to reject the segment itself rather than the
# resolved path.
run_case_type_error local-ruleset-traversal "outside the allowed rule-set roots" \
"$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json \
"s%^[[:space:]]*option path '.*/ruleset/test.srs'% option path '/etc/homeproxy-pro/ruleset/../../etc/shadow'%"
# 5c) A path INSIDE the archive that nobody has put a file at. This is the
# case the whole directory-timing fix is about: `sing-box check` opens
# every local rule_set path, so before the pre-check this produced
#
# parse rule-set[0]: open <path>: no such file or directory
#
# which names neither the rule-set the user created nor the directory they
# are meant to copy files into, and surfaced to them only as "new client
# configuration is invalid, reload aborted". The message below names both.
run_case_type_error local-ruleset-missing-file "which is missing, not a regular file, or empty" \
"$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json \
"s%^[[:space:]]*option path '\(.*\)/ruleset/test.srs'% option path '\1/ruleset/never-copied-here.srs'%"
# 5d) An out-of-policy initial_path is now a refusal too, instead of a silent
# null. The old behaviour dropped the field, so the rule-set went back to
# blocking startup on its first download and the configuration looked
# configured while behaving as if the field were empty.
#
# One sed script, two expressions: turn the local rule-set into a remote one
# and give it an initial_path outside the archive. The `path` line is
# replaced rather than left behind because a remote rule_set carries no
# `path`, and this case must fail on the whitelist rather than reach
# `sing-box check` with a field the remote type does not have.
run_case_type_error remote-ruleset-bad-initial-path "initial_path .* is outside the allowed rule-set roots" \
"$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json \
"s%option type 'local'%option type 'remote'%;s%^[[:space:]]*option path '.*/ruleset/test.srs'% option url 'https://example.invalid/x.srs'\n option initial_path '/etc/passwd'%"
# 7) Batch 1: the format probe and the duration normalisation.
#
# The `format` field describes a file's NAME, and sing-box infers it from
# the extension. That guess is wrong in three ways, and each of them ends
# the same way - `sing-box check` rejects the configuration, the reload is
# aborted and the user sees only "my change did not take":
#
# a) an explicit format naming the wrong one
# b) no explicit format, but the content disagrees with the extension
# c) no explicit format and no extension to infer from ("missing format")
#
# generate_client.uc reads the first bytes of the file and hands the
# verdict over as ruleset_formats; ruleset.uc's resolveFormat() decides
# what to emit. The archive holds a valid version-3 source rule-set under
# two names (see run_case) so these cases vary the NAME and the DECLARED
# value while the content stays valid.
#
# Each case asserts on the GENERATED JSON rather than on the log: the
# generated `format` is the thing sing-box is given, and a log line that
# appears while the JSON is still wrong would be a false pass.
# 7a) (a) declared binary, file is source JSON.
rs_wrong_decl="$WORK/ruleset-format-wrong-declared/run/sing-box-c.json"
run_case ruleset-format-wrong-declared "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json \
"s%^[[:space:]]*option path '\(.*\)/ruleset/test.srs'% option path '\1/ruleset/source.json'%"
if [ ! -f "$rs_wrong_decl" ]; then
echo "FAIL: ruleset-format-wrong-declared: no config was generated"
FAILED=1
elif ! grep -q '"tag": "cfg-rs_local-rule"' "$rs_wrong_decl"; then
echo "FAIL: ruleset-format-wrong-declared: the rule-set is missing from the config"
FAILED=1
# The entry is multi-line, so read the block rather than grepping the whole
# file: "format": "binary" appears on the built-in geoip-cn entry too.
elif ! awk '/"tag": "cfg-rs_local-rule"/,/^ },$/' "$rs_wrong_decl" | grep -q '"format": "source"'; then
echo "FAIL: ruleset-format-wrong-declared: a source-JSON file declared binary was not corrected:"
awk '/"tag": "cfg-rs_local-rule"/,/^ },$/' "$rs_wrong_decl" | sed 's/^/ /'
FAILED=1
else
echo "PASS: ruleset-format-wrong-declared: declared binary, file is source -> corrected to source"
fi
# 7b) (b) no declared format, content is source JSON under a .srs name. The
# extension inference is what lies here, so the case also has to prove the
# correction is ANNOUNCED - otherwise the user cannot tell why their
# empty field turned into an explicit one.
rs_mislabelled="$WORK/ruleset-format-mislabelled/run/sing-box-c.json"
run_case ruleset-format-mislabelled "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json \
"/^[[:space:]]*option format 'binary'\$/d;s%^[[:space:]]*option path '\(.*\)/ruleset/test.srs'% option path '\1/ruleset/mislabelled.srs'%"
if [ ! -f "$rs_mislabelled" ]; then
echo "FAIL: ruleset-format-mislabelled: no config was generated"
FAILED=1
elif ! awk '/"tag": "cfg-rs_local-rule"/,/^ },$/' "$rs_mislabelled" | grep -q '"format": "source"'; then
echo "FAIL: ruleset-format-mislabelled: a source-JSON file named .srs was not declared as source:"
awk '/"tag": "cfg-rs_local-rule"/,/^ },$/' "$rs_mislabelled" | sed 's/^/ /'
FAILED=1
else
echo "PASS: ruleset-format-mislabelled: source JSON under a .srs name -> declared source"
fi
# 7c) The case that must NOT change: a .srs holding a real .srs, with no
# declared format. sing-box's inference is already right, so emitting an
# explicit value would alter the generated bytes of every working
# rule-set for no behavioural gain. This is the assertion that keeps the
# correction from becoming a rewrite.
rs_untouched="$WORK/ruleset-format-untouched/run/sing-box-c.json"
run_case ruleset-format-untouched "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json \
"/^[[:space:]]*option format 'binary'\$/d"
if [ ! -f "$rs_untouched" ]; then
echo "FAIL: ruleset-format-untouched: no config was generated"
FAILED=1
elif awk '/"tag": "cfg-rs_local-rule"/,/^ },$/' "$rs_untouched" | grep -q '"format":'; then
echo "FAIL: ruleset-format-untouched: a correct rule-set gained an explicit format;"
echo " the probe must only speak when it disagrees with what sing-box would do"
awk '/"tag": "cfg-rs_local-rule"/,/^ },$/' "$rs_untouched" | sed 's/^/ /'
FAILED=1
else
echo "PASS: ruleset-format-untouched: an already-correct rule-set is left exactly as it was"
fi
# 7d) update_interval normalisation. sing-box parses it as a Go duration, and
# `update_interval: "3600"` is a measured hard failure on 1.14.2 ("time:
# missing unit in duration \"3600\"") that rejects the whole configuration.
# strToTime() appends the unit; "24h" must pass through untouched, because
# a value that already carries a unit is the normal case and must not be
# rewritten into something else.
rs_interval="$WORK/ruleset-update-interval/run/sing-box-c.json"
run_case ruleset-update-interval "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json \
"s%^[[:space:]]*option path '.*/ruleset/test.srs'%&\n\nconfig ruleset 'rs_remote'\n\toption enabled '1'\n\toption label 'remote-rs'\n\toption type 'remote'\n\toption format 'binary'\n\toption url 'https://example.invalid/x.srs'\n\toption update_interval '3600'%"
run_case ruleset-update-interval-unit "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json \
"s%^[[:space:]]*option path '.*/ruleset/test.srs'%&\n\nconfig ruleset 'rs_remote'\n\toption enabled '1'\n\toption label 'remote-rs'\n\toption type 'remote'\n\toption format 'binary'\n\toption url 'https://example.invalid/x.srs'\n\toption update_interval '24h'%"
for pair in "ruleset-update-interval:3600s:3600" "ruleset-update-interval-unit:24h:24h"; do
name="${pair%%:*}"; rest="${pair#*:}"; want="${rest%%:*}"; was="${rest#*:}"
f="$WORK/$name/run/sing-box-c.json"
if [ ! -f "$f" ]; then
echo "FAIL: $name: no config was generated"
FAILED=1
elif ! grep -q "\"update_interval\": \"$want\"" "$f"; then
echo "FAIL: $name: update_interval '$was' should be emitted as '$want':"
grep -n '"update_interval"' "$f" | sed 's/^/ /'
FAILED=1
else
echo "PASS: $name: update_interval '$was' is emitted as '$want'"
fi
done
# 8) Batch 2a: the "do not block startup on the first download" fallback.
#
# A remote rule-set with no initial_path is fetched during initialization,
# BEFORE the inbounds bind. On a cold cache - which is what a fresh install
# has, since cache.db ships empty - that means the first start waits on
# raw.githubusercontent.com, through the selected node, before the instance
# will listen at all; and a failure there is a FATAL that the health gate
# turns into a rollback or a released intercept layer.
#
# The opt-in points such a rule-set at an EMPTY rule-set, so sing-box reads
# a file, starts, and refreshes from the URL in the background. It is
# opt-in because an empty rule-set matches nothing and everything it would
# have split falls through to `final` - a routing change, not a robustness
# tweak.
#
# The first case is the one that matters most and is the easiest to get
# wrong: with the switch off, nothing may change. Not "the feature is off",
# but no initial_path, no file written, and a configuration that differs
# from the pre-feature one.
# 8a) The default. opt-in off -> no initial_path anywhere, and no fallback
# file created. The generated config must be identical to the same
# fixture with the option absent, which is the upgrade-invisible property.
run_case safe-start-off "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
run_case safe-start-off-explicit "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json \
"s/option log_level 'error'/option log_level 'error'\n\toption ruleset_safe_start '0'/"
for name in safe-start-off safe-start-off-explicit; do
f="$WORK/$name/run/sing-box-c.json"
if [ ! -f "$f" ]; then
echo "FAIL: $name: no config was generated"
FAILED=1
elif grep -q 'initial_path' "$f"; then
echo "FAIL: $name: the opt-in is OFF but the config carries an initial_path:"
grep -n 'initial_path' "$f" | sed 's/^/ /'
FAILED=1
elif [ -d "$WORK/$name/ruleset/initial" ] && [ -n "$(ls -A "$WORK/$name/ruleset/initial" 2>/dev/null | grep -v '^empty\.')" ]; then
echo "FAIL: $name: the opt-in is OFF but a fallback file was written:"
ls -1 "$WORK/$name/ruleset/initial" | sed 's/^/ /'
FAILED=1
else
echo "PASS: $name: opt-in off -> no initial_path, no fallback file"
fi
done
# 8b) opt-in on, the built-ins. These two ARE the feature: an earlier version
# of this file only knew how to build their entries, and the CLI had no
# idea they existed, so a cold install blocked on downloading them.
run_case safe-start-on "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json \
"s/option log_level 'error'/option log_level 'error'\n\toption ruleset_safe_start '1'/"
ss_json="$WORK/safe-start-on/run/sing-box-c.json"
ss_initial="$WORK/safe-start-on/ruleset/initial"
if [ ! -f "$ss_json" ]; then
echo "FAIL: safe-start-on: no config was generated"
FAILED=1
else
# grep -F on the fully-constructed path, not an awk range around the entry.
# The range form over-captured: it ran past the entry's closing brace into
# the next one and then reported success on whatever it found, so a
# rule-set with NO initial_path would have passed whenever the next entry
# had one. Asserting the exact string also pins that the path is inside
# this run's archive, which is the property that makes it readable by the
# jailed client and admitted by the path whitelist.
for tag in geoip-cn geosite-cn; do
want="\"initial_path\": \"$ss_initial/$tag.srs\""
if ! grep -qF "$want" "$ss_json"; then
echo "FAIL: safe-start-on: the built-in '$tag' does not carry"
echo " $want"
grep -n 'initial_path' "$ss_json" | sed 's/^/ /'
FAILED=1
elif [ ! -s "$ss_initial/$tag.srs" ]; then
echo "FAIL: safe-start-on: '$tag' points at an initial file that was not written"
FAILED=1
else
echo "PASS: safe-start-on: built-in '$tag' -> initial_path inside the archive, file present"
fi
done
# The file has to be a real compiled rule-set, not a copy of the source
# JSON and not an empty file: sing-box IGNORES an initial file whose format
# does not match the declared one and blocks startup exactly as if there
# were none, which would make the whole feature a no-op that looks enabled.
# The magic is the check - it is what distinguishes a .srs from source JSON.
for tag in geoip-cn geosite-cn; do
f="$ss_initial/$tag.srs"
[ -s "$f" ] || continue
if [ "$(dd if="$f" bs=1 count=3 2>/dev/null)" = "SRS" ]; then
echo "PASS: safe-start-on: $tag.srs is a compiled rule-set (SRS magic)"
else
echo "FAIL: safe-start-on: $tag.srs is not a compiled rule-set - the first three"
echo " bytes are not the SRS magic, so sing-box would ignore the initial file"
FAILED=1
fi
done
fi
# 8c/8d) opt-in on, user-defined remote rule-sets: one tag and two tags.
# One fixture covers both, plus a disabled one:
# tests/fixtures/generators/custom_safe_start.uci. A separate file rather
# than sed variations of custom.uci, because the opt-in belongs to the
# `config homeproxy-pro 'routing'` section - a different section than anything
# the existing variations anchor on - and because a two-expression sed that
# both inserts an option and appends a whole section is a construct this
# suite has never run. The one-expression form is proven here; the
# two-expression form is not, and a test whose own tooling is untested is
# how a case quietly stops testing anything.
run_case safe-start-user-remote "$ROOT/tests/fixtures/generators/custom_safe_start.uci" \
generate_client.uc sing-box-c.json
sr_json="$WORK/safe-start-user-remote/run/sing-box-c.json"
sr_initial="$WORK/safe-start-user-remote/ruleset/initial"
if [ ! -f "$sr_json" ]; then
echo "FAIL: safe-start-user-remote: no config was generated"
FAILED=1
else
# Single tag: a concrete path, and the file it names has to be there. A
# pointer at a file that does not exist is not a degraded feature, it is no
# feature - sing-box ignores it and blocks startup exactly as before.
# Same reason as 8b: assert the exact string, not a range that can run on
# into the next entry.
sr_want="\"initial_path\": \"$sr_initial/cfg-rs_remote-rule.srs\""
if ! grep -qF "$sr_want" "$sr_json"; then
echo "FAIL: safe-start-user-remote: the single-tag rule-set does not carry"
echo " $sr_want"
grep -n 'initial_path' "$sr_json" | sed 's/^/ /'
FAILED=1
fi
if [ -s "$sr_initial/cfg-rs_remote-rule.srs" ]; then
echo "PASS: safe-start-user-remote: single-tag -> concrete initial_path, file present"
else
echo "FAIL: safe-start-user-remote: no fallback file for cfg-rs_remote-rule.srs"
FAILED=1
fi
# Multi tag: {tag} in the path, and one file per tag. The entry is matched
# on the multi-tag shape (a tag ARRAY) rather than on the section name, so
# the assertion cannot be satisfied by the single-tag entry above.
mt_want="\"initial_path\": \"$sr_initial/{tag}.srs\""
if ! grep -qF "$mt_want" "$sr_json"; then
echo "FAIL: safe-start-user-remote: the multi-tag rule-set does not carry"
echo " $mt_want"
echo " sing-box substitutes {tag} and opens every resulting file, so a"
echo " literal path would make it fetch and block - the failure this removes"
grep -n 'initial_path' "$sr_json" | sed 's/^/ /'
FAILED=1
fi
mt_missing=""
for tag in cfg-rs_multi-rule cfg-alt-rule; do
[ -s "$sr_initial/$tag.srs" ] || mt_missing="$mt_missing $tag"
done
if [ -n "$mt_missing" ]; then
echo "FAIL: safe-start-user-remote: no fallback file for:$mt_missing"
echo " sing-box opens every tag's file, so a missing one blocks startup"
FAILED=1
else
echo "PASS: safe-start-user-remote: multi-tag -> {tag} placeholder, one file per tag"
fi
# Disabled: build_user_rulesets() skips it, so it must be absent from the
# config AND must not have caused a file to be written for it.
if grep -q 'cfg-rs_off-rule' "$sr_json"; then
echo "FAIL: safe-start-user-remote: a disabled rule-set reached the generated config"
FAILED=1
elif [ -e "$sr_initial/cfg-rs_off-rule.srs" ]; then
echo "FAIL: safe-start-user-remote: a fallback file was written for a disabled rule-set"
FAILED=1
else
echo "PASS: safe-start-user-remote: a disabled rule-set gets neither path nor file"
fi
fi
# 6) P3 #8 (extra_tags die() on missing {tag}): deferred to
# a dedicated 'testbed-dialect' sprint. The multi-line sed to
# attach an extra_tags list onto the existing rs_local ruleset
@@ -12,7 +12,8 @@
'use strict';
import { lsdir } from 'fs';
import { executeCommand, isValidCIDR, isValidPEM, redactReason, redactUrl, shellQuote, wGETVerbose } from 'homeproxy-pro';
import { executeCommand, isValidCIDR, isValidPEM, redactReason, redactUrl, ruleSetFormatFromBytes,
ruleSetFormatFromPath, RULESET_PROBE_BYTES, shellQuote, wGETVerbose } from 'homeproxy-pro';
let failures = 0,
checks = 0;
@@ -75,45 +76,95 @@ expect('pem.key-as-cert', isValidPEM(pem_key, false), false);
expect('pem.garbage', isValidPEM('not a pem at all', false), false);
expect('pem.empty', isValidPEM('', false), false);
/* wGETVerbose() must not hand wget an option the target's wget does not have.
* It used to pass --max-filesize, which busybox wget does not support: wget
* exited 2 with "unrecognized option" before making a request, so every
* subscription fetch failed while the test suite stayed green (the fetcher
* tests mock wGETVerbose itself). A closed local port makes the fetch fail
* fast either way; what is asserted is that it fails for a network reason and
* not a usage one. */
const wget = wGETVerbose('http://127.0.0.1:1/never');
expect('wget.not-a-usage-error',
match(wget.error || '', /unrecognized option|invalid option|Usage:/) == null, true);
expect('wget.reports-a-reason', length(wget.error || '') > 0, true);
expect('wget.no-content-on-failure', wget.content, null);
/* wGETVerbose() must not hand the fetcher an option the fetcher does not
* have.
*
* This guard already existed once, for --max-filesize, and then failed to catch
* the same class again: the fetch shelled out to `wget` with GNU-only options
* (-nv --user-agent --timeout=), and `wget` is whichever implementation the
* firmware's buildroot compiled. On a busybox-wget router every one of those is
* rejected with "unrecognized option" before a request is made, so every
* subscription fetch, every resource update and the connectivity check failed -
* while this test ran against GNU wget on CI and on the maintainer's own
* device, and stayed green.
*
* So the guard now runs against tests/fixtures/uclient-fetch-stub, which
* accepts exactly the option list read off the applet on a real device and
* rejects everything else the way the applet does. The accepted set is PINNED
* rather than inherited from the host, which is the part that was missing.
* (On a target, where /bin/uclient-fetch really exists, fetchBinary() returns
* it and these same assertions run against the real applet - so an option
* nobody verified is caught both off-target and on it.)
*
* A closed local port makes the fetch fail fast either way; what is asserted is
* that it fails for a NETWORK reason and not a usage one. */
const fetch = wGETVerbose('http://127.0.0.1:1/never');
expect('fetch.not-a-usage-error',
match(fetch.error || '', /unrecognized option|invalid option|Usage:/) == null, true);
expect('fetch.reports-a-reason', length(fetch.error || '') > 0, true);
expect('fetch.no-content-on-failure', fetch.content, null);
/* Review H3: an HTTP-level wget failure echoes the target, query string and
* all, so `<URL>?token=secret: 404 Not Found` is what would reach the log.
* A *connection* failure is the cheap case to reproduce here, but wget prints
* only `failed: Connection refused.` for it - no URL - so this fetch proves
* the end-to-end path hands back no token, not that redaction ran. The
* redactReason() block below covers redaction itself, using the shapes wget
* actually emits. */
const tokwget = wGETVerbose('http://127.0.0.1:1/?token=secret');
expect('wget.token-not-in-error',
match(tokwget.error || '', /token=secret/) == null, true);
/* A 200 whose body is binary. executeCommand() nulls stdout for binary
* content, and the "no content but stderr said something" branch then used to
* report it as `fetch failed: … Download completed (34185 bytes)` - a message
* that contradicts itself and sends the user after a network problem they do
* not have. Measured on a device against a real .srs. */
const binfetch = wGETVerbose('http://127.0.0.1:1/HP_T_STUB_BINARY');
expect('fetch.binary-is-not-a-failure',
match(binfetch.error || '', /fetch failed/) == null, true, binfetch.error);
expect('fetch.binary-says-so',
match(binfetch.error || '', /binary/) != null, true, binfetch.error);
expect('fetch.binary-has-no-content', binfetch.content, null);
/* Review H3: the fetcher announces the requested URL on stderr before it reports
* anything else, query string and all, so this is what would reach the log.
*
* The old version of this test pointed at a *connection* failure, where the URL
* happens not to appear - which meant it proved only that the quiet path stayed
* quiet, never that redaction ran. The stub announces the URL the way the real
* applet does, so this now exercises redactReason() through the whole
* wGETVerbose() path. The redactReason() block below still covers redaction on
* its own, against shapes no stub can be asked to produce. */
const tokfetch = wGETVerbose('http://127.0.0.1:1/?token=secret');
expect('fetch.token-not-in-error',
match(tokfetch.error || '', /token=secret/) == null, true);
/* Redaction must not swallow the diagnostic: the target host still has to be
* readable, or the message says nothing about what was being fetched. */
expect('fetch.redacted-still-names-the-target',
match(tokfetch.error || '', /127\.0\.0\.1/) != null, true, tokfetch.error);
/* The HTTP-error shape is the one that leaks - "Downloading '<URL>'" followed
* by "HTTP error 404" - and it is the common case for a subscription URL that
* has expired. */
const httpfetch = wGETVerbose('http://127.0.0.1:1/HP_T_STUB_HTTP_ERROR?token=secret');
expect('fetch.http-error-token-not-in-error',
match(httpfetch.error || '', /token=secret/) == null, true);
expect('fetch.http-error-keeps-the-status',
match(httpfetch.error || '', /HTTP error/) != null, true, httpfetch.error);
/* redactReason(): central redaction that protects every wGETVerbose caller.
* Tested in isolation so the assertion does not depend on wget being
* Tested in isolation so the assertion does not depend on any fetcher being
* present - this runs anywhere ucode runs. */
{
/* Canonical wget failure shape: '<URL>: <reason>' - the URL must lose
* its query string and userinfo. */
const r1 = redactReason('https://user:tok@host.example.com/path?q=token=secret: Bad port \'80080\'.');
/* The shape uclient-fetch actually emits, copied from a device run:
*
* Downloading 'https://user:tok@host.example.com/path?q=token=secret'
* HTTP error 404
*
* It announces the target BEFORE it says anything else, which is why
* wGETVerbose does not pass -q - and it is therefore the shape that has
* to be redacted. The URL must lose its query string and its userinfo,
* and the host has to survive, or the message says nothing about what was
* being fetched. */
const r1 = redactReason("Downloading 'https://user:tok@host.example.com/path?q=token=secret' HTTP error 404");
expect('redactReason.query',
match(r1, /\?\*\*\*/) != null, true);
match(r1, /\?\*\*\*/) != null, true, r1);
expect('redactReason.userinfo',
match(r1, /user:tok/) == null, true);
match(r1, /user:tok/) == null, true, r1);
expect('redactReason.host-preserved',
match(r1, /host\.example\.com/) != null, true);
match(r1, /host\.example\.com/) != null, true, r1);
expect('redactReason.reason-preserved',
match(r1, /Bad port/) != null, true);
match(r1, /HTTP error 404/) != null, true, r1);
/* A second URL in the same message - both must be redacted. */
const r2 = redactReason('first https://a.example/?token=A then https://b.example/?token=B end');
@@ -122,7 +173,8 @@ expect('wget.token-not-in-error',
/* No URL at all: returned unchanged. */
expect('redactReason.no-url-unchanged',
redactReason('wget: bad port'), 'wget: bad port');
redactReason('Failed to send request: Operation not permitted'),
'Failed to send request: Operation not permitted');
/* Empty / non-string: returned unchanged (the function is safe to
* apply to the trimmed stderr without a guard). */
@@ -132,7 +184,7 @@ expect('wget.token-not-in-error',
/* URL with port: the port must survive (the path is what we keep;
* only query and userinfo go). */
const r3 = redactReason('wget: https://host:80080/path?q=token=secret: failed');
const r3 = redactReason("Downloading 'https://host:80080/path?q=token=secret' HTTP error 500");
expect('redactReason.port-survives',
match(r3, /host:80080/) != null, true);
expect('redactReason.port-query-redacted',
@@ -175,6 +227,62 @@ expect('cidr6.prefix-injection',isValidCIDR('::1/64;}', 6), false);
expect('cidr6.two-colons', isValidCIDR('1::2::3', 6), false);
expect('cidr6.empty', isValidCIDR('', 6), false);
/* --- the rule-set format probe -------------------------------------------
*
* Two pure functions, so the whole decision is testable without a file and
* without sing-box: ruleSetFormatFromBytes() looks at bytes, and
* ruleSetFormatFromPath() says what sing-box's extension inference would
* have decided for a name. The pairing is the point - the generator compares
* the two (generator/ruleset.uc's resolveFormat) and only speaks when they
* disagree - so a regression in either half has to show up here.
*
* "SRS" is 0x53 0x52 0x53. It is written as a literal because that IS the
* byte sequence, and the test is about the function answering correctly for
* the format's own identity, not about how the constant was spelled.
*
* Returns are 'binary' | 'source' | null, and the null cases carry most of
* the weight: the probe must decline to have an opinion rather than guess,
* because a guess here becomes a `format` declaration that makes sing-box
* reject the configuration at startup. */
expect('probe: SRS magic is binary', ruleSetFormatFromBytes('SRS' + 'x', 4), 'binary');
expect('probe: a three-byte SRS is binary', ruleSetFormatFromBytes('SRS', 3), 'binary');
expect('probe: a JSON object is source', ruleSetFormatFromBytes('{"version":3,"rules":[]}', 21), 'source');
/* Leading whitespace is the normal shape of a file written by an editor or a
* Windows tool, and the decision must not depend on byte 0 being '{'. */
expect('probe: leading whitespace is still source',
ruleSetFormatFromBytes('\n\t {"version":3}', 14), 'source');
expect('probe: a JSON array is source', ruleSetFormatFromBytes('[{"a":1}]', 8), 'source');
/* Every null case is a "do not touch the user's field" case. */
expect('probe: an empty file is no verdict', ruleSetFormatFromBytes('', 0), null);
expect('probe: a failed read is no verdict', ruleSetFormatFromBytes('', 10), null);
expect('probe: two bytes are not the magic', ruleSetFormatFromBytes('SR', 2), null);
expect('probe: a near-miss magic is no verdict', ruleSetFormatFromBytes('SRX', 3), null);
/* The one that matters most: prose is text and is still not a source
* rule-set. If "looks like text" were the test, this would answer 'source'
* and the generated config would name a JSON file that does not exist. */
expect('probe: printable text is no verdict', ruleSetFormatFromBytes('hello world', 11), null);
/* What a failed download actually leaves behind under a .srs name. */
expect('probe: an HTML error page is no verdict',
ruleSetFormatFromBytes('<!DOCTYPE html><html></html>', 27), null);
expect('probe: the read window is a sane size',
RULESET_PROBE_BYTES >= 8 && RULESET_PROBE_BYTES <= 4096, true);
/* What sing-box's extension inference would have decided, expressed in the
* same two values. null is the case that produces "missing format" rather
* than a wrong parse, and it is the reason a content probe is worth having. */
expect('inference: .srs is binary', ruleSetFormatFromPath('/etc/homeproxy-pro/ruleset/example.srs'), 'binary');
expect('inference: .json is source', ruleSetFormatFromPath('/etc/homeproxy-pro/ruleset/example.json'), 'source');
expect('inference: no extension has no verdict', ruleSetFormatFromPath('/etc/homeproxy-pro/ruleset/example'), null);
/* The suffix is what counts, not the first dot in the name. */
expect('inference: .srs.txt is no verdict', ruleSetFormatFromPath('/etc/homeproxy-pro/ruleset/x.srs.txt'), null);
expect('inference: the suffix is case-sensitive', ruleSetFormatFromPath('/etc/homeproxy-pro/ruleset/x.SRS'), null);
/* A {tag} placeholder comes before the suffix, so it must not hide it. */
expect('inference: a {tag} path keeps its suffix', ruleSetFormatFromPath('/etc/homeproxy-pro/ruleset/{tag}.srs'), 'binary');
expect('inference: null has no verdict', ruleSetFormatFromPath(null), null);
expect('inference: an empty path has no verdict', ruleSetFormatFromPath(''), null);
/* descriptors must not leak across calls */
const before = fd_count();
for (let i = 0; i < 200; i++)
@@ -21,7 +21,7 @@
'use strict';
import { buildTLSObject, buildTransportObject, validateHomeProxyPath, validateCertificatePath, HP_DIR } from 'homeproxy-pro';
import { buildTLSObject, buildTransportObject, rulePathRootsText, validateCertificatePath, validateHomeProxyPath, validateRuleSetPath, HP_DIR } from 'homeproxy-pro';
let failures = 0,
checks = 0;
@@ -100,6 +100,54 @@ expect('cert.paths empty rejected', validateCertificatePath(''), false);
* /tmp/homeproxy_ (that root is for upload staging). */
expect('rule-set path still rejects /etc/ssl', validateHomeProxyPath('/etc/ssl/certs/srv.pem'), false);
/* 4c. Rule-set source files have a THIRD policy, and it is the narrowest of
* the three. validateHomeProxyPath() accepts everything under
* /etc/homeproxy-pro/ and everything under /tmp/homeproxy_ - which is right
* for a general "this path is inside the package" gate and wrong for a
* rule-set, because a rule-set has exactly one home: the archive the
* package creates (RULE_PATH_ROOTS, mirrored by HP_RULE_PATH_ROOTS and
* locked across the JS/ucode boundary by guard 50).
*
* The distinction that matters: validateHomeProxyPath() still answers
* true for /etc/homeproxy-pro/ruleset/x.srs AND for /tmp/homeproxy_x/y.srs,
* while validateRuleSetPath() answers true only for the first. A
* frontend built on the general gate would offer paths the generator
* then refuses - the guard-29 failure mode, one policy over.
*/
expect('ruleset path accepts the archive',
validateRuleSetPath('/etc/homeproxy-pro/ruleset/example.srs'), true);
expect('ruleset path rejects a bare root',
validateRuleSetPath('/etc/homeproxy-pro/ruleset/'), false);
expect('ruleset path rejects /etc/passwd',
validateRuleSetPath('/etc/passwd'), false);
expect('ruleset path rejects another path under /etc/homeproxy-pro',
validateRuleSetPath('/etc/homeproxy-pro/resources/china_ip4.json'), false);
expect('ruleset path rejects the certs directory',
validateRuleSetPath('/etc/homeproxy-pro/certs/server_privatekey.pem'), false);
expect('ruleset path rejects /tmp/homeproxy_ upload staging',
validateRuleSetPath('/tmp/homeproxy_ruleset_upload.tmp'), false);
expect('ruleset path rejects /etc/ssl',
validateRuleSetPath('/etc/ssl/certs/srv.pem'), false);
expect('ruleset path rejects a traversal out of the archive',
validateRuleSetPath('/etc/homeproxy-pro/ruleset/../../etc/shadow'), false);
expect('ruleset path rejects a relative path',
validateRuleSetPath('etc/homeproxy-pro/ruleset/x.srs'), false);
expect('ruleset path rejects null', validateRuleSetPath(null), false);
expect('ruleset path rejects empty', validateRuleSetPath(''), false);
expect('ruleset path rejects a non-string', validateRuleSetPath(42), false);
/* And the general gate is unchanged: narrowing the rule-set policy must not
* have narrowed the one buildTLSObject() and the parser still rely on. */
expect('general gate still accepts /etc/homeproxy-pro',
validateHomeProxyPath('/etc/homeproxy-pro/ruleset/example.srs'), true);
expect('general gate still accepts /tmp/homeproxy_',
validateHomeProxyPath('/tmp/homeproxy_cert_server_publickey.tmp'), true);
/* The diagnostic has to name the roots, or the user is left guessing where the
* file is supposed to go. */
expect('the roots text lists the archive',
!!match(rulePathRootsText(), /\/etc\/homeproxy-pro\/ruleset\//), true);
const sslCert = buildTLSObject({ enabled: '1', server_name: 's', cert_path: '/etc/ssl/certs/srv.pem' }, false);
expect('client.cert_path /etc/ssl kept', sslCert.certificate_path, '/etc/ssl/certs/srv.pem');
const sslServerKey = buildTLSObject({ enabled: '1', server_name: 's', cert_path: '/etc/acme/c.pem' }, true,
+9 -1
View File
@@ -8,12 +8,20 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-lucky
PKG_VERSION:=1.0.4
PKG_RELEASE:=2
PKG_RELEASE:=3
LUCI_TITLE:=LuCI Support for lucky
LUCI_DEPENDS:=+lucky
LUCI_PKGARCH:=all
define Package/$(PKG_NAME)/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
(sleep 1; /etc/init.d/rpcd reload) >/dev/null 2>&1 &
}
exit 0
endef
define Package/$(PKG_NAME)/prerm
#!/bin/sh
rm -f /tmp/lucky_info_cache 2>/dev/null
@@ -3,6 +3,7 @@
'require rpc';
'require uci';
'require ui';
'require dom';
'require form';
var callGetInfo = rpc.declare({ object: 'luci.lucky', method: 'get_info', expect: { } });
@@ -19,20 +20,43 @@ return view.extend({
handleUpdate: function(key, value) {
ui.showModal(null, [ E('p', { class: 'spinning' }, _('Updating configuration and restarting service...')) ]);
var self = this;
return callSetConfig(key, value).then(function(res) {
if (res && res.ret === 0) {
return callService('restart').then(function() {
// Wait a bit for restart
setTimeout(function() {
ui.hideModal();
ui.addNotification(null, E('p', _('Configuration updated successfully')), 'info');
window.location.reload();
}, 2000);
var startTime = Date.now();
function waitForReady() {
if (Date.now() - startTime > 10000) {
ui.hideModal();
ui.addNotification(null, E('p', _('Configuration updated successfully')), 'info');
self.load().then(function(data) {
var node = self.render(data);
var map = document.querySelector('.cbi-map');
if (map && map.parentNode) dom.content(map.parentNode, node);
});
return;
}
callGetInfo().then(function(info) {
if (info && info.running) {
ui.hideModal();
ui.addNotification(null, E('p', _('Configuration updated successfully')), 'info');
self.load().then(function(data) {
var node = self.render(data);
var map = document.querySelector('.cbi-map');
if (map && map.parentNode) dom.content(map.parentNode, node);
});
} else {
setTimeout(waitForReady, 500);
}
});
}
setTimeout(waitForReady, 500);
});
} else {
ui.hideModal();
ui.addNotification(null, E('p', _('Update failed')), 'error');
var errMsg = (res && res.error) ? res.error : _('Update failed');
ui.addNotification(null, E('p', errMsg), 'error');
}
});
},
@@ -63,7 +87,7 @@ return view.extend({
o.rmempty = false;
o.placeholder = '/etc/config/lucky.daji';
// Admin Panel Settings - 使用同一个 TypedSection 追加虚拟选项
// Admin Panel Settings
s = m.section(form.TypedSection, 'lucky', _('Admin Panel Settings'));
s.anonymous = true;
@@ -72,8 +96,13 @@ return view.extend({
o.datatype = 'port';
o.load = function(section_id) { return baseConf.AdminWebListenPort || "16601"; };
o.write = function(section_id, value) {
callSetConfig('admin_http_port', value);
return this.super('write', [section_id, value]);
var self = this;
return callSetConfig('admin_http_port', value).then(function(res) {
if (res && res.ret !== 0 && res.error) {
ui.addNotification(null, E('p', res.error), 'error');
}
return self.super('write', [section_id, value]);
});
};
o.remove = function(section_id) {
return this.super('remove', [section_id]);
@@ -83,12 +112,19 @@ return view.extend({
o = s.option(form.Value, 'admin_safe_url', _('Admin Safe URL'));
o.load = function(section_id) { return baseConf.SafeURL || ""; };
o.write = function(section_id, value) {
callSetConfig('admin_safe_url', value);
return this.super('write', [section_id, value]);
var self = this;
return callSetConfig('admin_safe_url', value).then(function(res) {
if (res && res.ret !== 0 && res.error) {
ui.addNotification(null, E('p', res.error), 'error');
}
return self.super('write', [section_id, value]);
});
};
o.remove = function(section_id) {
callSetConfig('admin_safe_url', '');
return this.super('remove', [section_id]);
var self = this;
return callSetConfig('admin_safe_url', '').then(function() {
return self.super('remove', [section_id]);
});
};
// Allow Internet Access
@@ -97,12 +133,16 @@ return view.extend({
return baseConf.AllowInternetaccess ? '1' : '0';
};
o.write = function(section_id, value) {
callSetConfig('switch_Internetaccess', (value === '1'));
return this.super('write', [section_id, value]);
var self = this;
return callSetConfig('switch_Internetaccess', (value === '1')).then(function() {
return self.super('write', [section_id, value]);
});
};
o.remove = function(section_id) {
callSetConfig('switch_Internetaccess', false);
return this.super('remove', [section_id]);
var self = this;
return callSetConfig('switch_Internetaccess', false).then(function() {
return self.super('remove', [section_id]);
});
};
// Reset Auth
@@ -183,8 +183,9 @@ return view.extend({
var self = this;
poll.add(function() {
return callGetStatus().then(function(res) {
return callGetInfo().then(function(res) {
self.updateStatus(res);
self.updateInfo(res);
});
}, 5);
+119
View File
@@ -0,0 +1,119 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "Admin Panel"
msgstr ""
msgid "Admin Panel Settings"
msgstr ""
msgid "Admin Safe URL"
msgstr ""
msgid "Admin Web Listen Port"
msgstr ""
msgid "Allow Internet Access"
msgstr ""
msgid "Application Information"
msgstr ""
msgid "Architecture"
msgstr ""
msgid "Are you sure you want to reset the admin account and password to default (666:666)?"
msgstr ""
msgid "Compile Time"
msgstr ""
msgid "Config Directory Path"
msgstr ""
msgid "Configuration updated successfully"
msgstr ""
msgid "Configure basic settings for the Lucky high-performance routing service, including master enable switch and administrative ports."
msgstr ""
msgid "Current Status"
msgstr ""
msgid "Enable"
msgstr ""
msgid "Executing service action..."
msgstr ""
msgid "General Settings"
msgstr ""
msgid "Loading logs..."
msgstr ""
msgid "Loading..."
msgstr ""
msgid "Log"
msgstr ""
msgid "Logs"
msgstr ""
msgid "Lucky"
msgstr ""
msgid "No log available."
msgstr ""
msgid "Operation timeout, please refresh the page"
msgstr ""
msgid "Overview"
msgstr ""
msgid "Overview of your Lucky routing service status, real-time statistics, and control settings."
msgstr ""
msgid "Real-time query of the Lucky running logs to diagnose DDNS updates, port forwarding actions, and routing issues."
msgstr ""
msgid "Reset Account and Password"
msgstr ""
msgid "Reset to Default (666:666)"
msgstr ""
msgid "Restart"
msgstr ""
msgid "Running"
msgstr ""
msgid "Running (Unmanaged)"
msgstr ""
msgid "Service Status"
msgstr ""
msgid "Service action executed successfully"
msgstr ""
msgid "Service not running"
msgstr ""
msgid "Settings"
msgstr ""
msgid "Stopped"
msgstr ""
msgid "Update failed"
msgstr ""
msgid "Updating configuration and restarting service..."
msgstr ""
msgid "Version"
msgstr ""
+84 -218
View File
@@ -1,253 +1,119 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "Lucky"
msgstr "Lucky"
msgid "IPv4/IPv6 port forwarding, DDNS, HTTP/HTTPS reverse proxy, WOL and more."
msgstr "IPv4/IPv6端口转发, 动态域名服务, HTTP/HTTPS反向代理, 网络唤醒以及更多功能。"
msgid "Overview"
msgstr "概览"
msgid "Settings"
msgstr "设置"
msgid "Log"
msgstr "日志"
msgid "Service Status"
msgstr "服务状态"
msgid "Service Control"
msgstr "服务控制"
msgid "Main Program Information"
msgstr "主程序信息"
msgid "Admin Panel Information"
msgstr "管理面板信息"
msgid "Enable"
msgstr "启用"
msgid "Basic Settings"
msgstr "基础设置"
msgid "Running Logs"
msgstr "运行日志"
msgid "Installation Status"
msgstr "安装状态"
msgid "Lucky Status"
msgstr "Lucky 运行状态"
msgid "Lucky Arch"
msgstr "程序架构"
msgid "Compilation Time"
msgstr "编译时间"
msgid "Lucky Version"
msgstr "程序版本"
msgid "Collecting data..."
msgstr "正在收集数据..."
msgid "Running"
msgstr "运行中"
msgid "Not running"
msgstr "未运行"
msgid "Not installed"
msgstr "未安装"
msgid "Installed"
msgstr "已安装"
msgid "Start"
msgstr "启动"
msgid "Stop"
msgstr "停止"
msgid "Restart"
msgstr "重启"
msgid "Processing..."
msgstr "处理中..."
msgid "Admin Panel"
msgstr "Lucky 后台管理"
msgstr "管理后台"
msgid "Admin Panel Login Info"
msgstr "管理后台登录信息"
msgid "Lucky Admin Http Port"
msgstr "Lucky 管理端口"
msgid "Admin Panel Settings"
msgstr "管理后台设置"
msgid "Admin Safe URL"
msgstr "管理安全入口"
msgid "Allow Internet access"
msgstr "允许外网访问管理界面"
msgid "Service not running, Admin Panel is unavailable"
msgstr "服务未运行,管理后台不可用"
msgid "DefaultAuth"
msgstr "默认登录账号密码"
msgid "Reset Account and Password"
msgstr "重置帐号密码"
msgid "update success"
msgstr "更新成功"
msgid "update failed"
msgstr "更新失败"
msgid "Updating configuration..."
msgstr "正在更新配置..."
msgid "Change"
msgstr "修改"
msgid "allow"
msgstr "允许"
msgid "not allow"
msgstr "禁止"
msgid "Are you sure Enalbe Internetaccess?"
msgstr "确定要启用外网访问吗?"
msgid "Are you sure Disable Internetaccess?"
msgstr "确定要禁用外网访问吗?"
msgid "Config dir path"
msgstr "配置文件夹路径"
msgid "The path to store the config file"
msgstr "存储 Lucky 配置文件的目录路径"
msgid "get latest version"
msgstr "获取最新版本"
msgid "portValueError"
msgstr "端口值无效"
msgid "No log available."
msgstr "暂无可用日志。"
msgid "Fetching logs..."
msgstr "正在拉取日志..."
msgid "Executing service action..."
msgstr "正在执行服务操作..."
msgid "Operation timeout, please refresh the page"
msgstr "操作超时,请刷新页面"
msgid "Service action executed successfully"
msgstr "服务操作执行成功"
msgid "Stopped"
msgstr "已停止"
msgid "Service not running"
msgstr "服务未运行"
msgid "Lucky Overview"
msgstr "Lucky 概览"
msgid "Monitor and control the Lucky service."
msgstr "监控并控制 Lucky 服务的运行状态。"
msgid "Loading..."
msgstr "加载中..."
msgid "Current Status"
msgstr "当前状态"
msgid "Actions"
msgstr "操作"
msgid "Application Information"
msgstr "应用程序信息"
msgid "Version"
msgstr "版本"
msgid "Architecture"
msgstr "系统架构"
msgid "Compile Time"
msgstr "编译时间"
msgid "Updating configuration and restarting service..."
msgstr "正在更新配置并重启服务..."
msgid "Configuration updated successfully"
msgstr "配置更新成功"
msgid "Update failed"
msgstr "更新失败"
msgid "Lucky Settings"
msgstr "Lucky 设置"
msgid "Configure basic settings for the Lucky service."
msgstr "配置 Lucky 服务的基础设置。"
msgid "General Settings"
msgstr "常规设置"
msgid "Config Directory Path"
msgstr "配置文件路径"
msgid "Admin Panel Settings"
msgstr "管理面板设置"
msgid "Admin Web Listen Port"
msgstr "后台管理端口"
msgid "Allow Internet Access"
msgstr "允许外网访问"
msgid "Reset to Default (666:666)"
msgstr "恢复默认 (666:666)"
msgid "Application Information"
msgstr "应用程序信息"
msgid "Architecture"
msgstr "系统架构"
msgid "Are you sure you want to reset the admin account and password to default (666:666)?"
msgstr "确定要将管理员账号和密码恢复为默认值 (666:666) 吗?"
msgid "Loading logs..."
msgstr "正在加载日志..."
msgid "Compile Time"
msgstr "编译时间"
msgid "Lucky Running Logs"
msgstr "Lucky 运行日志"
msgid "Config Directory Path"
msgstr "配置文件路径"
msgid "View the real-time execution logs of the Lucky service."
msgstr "查看 Lucky 服务的实时执行日志。"
msgid "Logs"
msgstr "日志"
msgid "Overview of your Lucky routing service status, real-time statistics, and control settings."
msgstr "展示 Lucky 服务当前的后台运行状态、系统架构、接口监听端口以及便捷的管理面板快捷登录链接。"
msgid "Configuration updated successfully"
msgstr "配置更新成功"
msgid "Configure basic settings for the Lucky high-performance routing service, including master enable switch and administrative ports."
msgstr "配置 Lucky 高性能分流与网络转发服务的核心基础参数,控制服务总开关、前台管理端口以及底层二进制运行策略。"
msgid "Current Status"
msgstr "当前状态"
msgid "Enable"
msgstr "启用"
msgid "Executing service action..."
msgstr "正在执行服务操作..."
msgid "General Settings"
msgstr "常规设置"
msgid "Loading logs..."
msgstr "正在加载日志..."
msgid "Loading..."
msgstr "加载中..."
msgid "Log"
msgstr "日志"
msgid "Logs"
msgstr "日志"
msgid "Lucky"
msgstr "Lucky"
msgid "No log available."
msgstr "暂无可用日志。"
msgid "Operation timeout, please refresh the page"
msgstr "操作超时,请刷新页面"
msgid "Overview"
msgstr "概览"
msgid "Overview of your Lucky routing service status, real-time statistics, and control settings."
msgstr "展示 Lucky 服务当前的后台运行状态、系统架构、接口监听端口以及便捷的管理面板快捷登录链接。"
msgid "Real-time query of the Lucky running logs to diagnose DDNS updates, port forwarding actions, and routing issues."
msgstr "实时查看 Lucky 的 system 运行日志,以快速排查动态域名 DDNS 更新状态、端口转发链路以及路由策略异常。"
msgid "Reset Account and Password"
msgstr "重置帐号密码"
msgid "Reset to Default (666:666)"
msgstr "恢复默认 (666:666)"
msgid "Restart"
msgstr "重启"
msgid "Running"
msgstr "运行中"
msgid "Running (Unmanaged)"
msgstr "运行中 (非托管)"
msgid "Service Status"
msgstr "服务状态"
msgid "Service action executed successfully"
msgstr "服务操作执行成功"
msgid "Service not running"
msgstr "服务未运行"
msgid "Settings"
msgstr "设置"
msgid "Stopped"
msgstr "已停止"
msgid "Update failed"
msgstr "更新失败"
msgid "Updating configuration and restarting service..."
msgstr "正在更新配置并重启服务..."
msgid "Version"
msgstr "版本"
@@ -1,6 +1,5 @@
#!/bin/sh
chmod +x /usr/bin/luckyarch
rm -f /tmp/luci-indexcache* 2>/dev/null
rm -f /tmp/luci-modulecache/* 2>/dev/null
rm -f /tmp/lucky_info_cache 2>/dev/null
-14
View File
@@ -1,14 +0,0 @@
#!/bin/sh
cputype=$(uname -ms | tr ' ' '_' | tr '[A-Z]' '[a-z]')
[ -n "$(echo $cputype | grep -E "linux.*armv.*")" ] && cpucore="armv5"
[ -n "$(echo $cputype | grep -E "linux.*armv7.*")" ] && [ -n "$(cat /proc/cpuinfo | grep vfp)" ] && [ ! -d /jffs/clash ] && cpucore="armv7"
[ -n "$(echo $cputype | grep -E "linux.*aarch64.*|linux.*armv8.*")" ] && cpucore="arm64"
[ -n "$(echo $cputype | grep -E "linux.*86.*")" ] && cpucore="i386"
[ -n "$(echo $cputype | grep -E "linux.*86_64.*")" ] && cpucore="x86_64"
if [ -n "$(echo $cputype | grep -E "linux.*mips.*")" ];then
mipstype=$(echo -n I | hexdump -o 2>/dev/null | awk '{ print substr($2,6,1); exit}') #通过判断大小端判断mips或mipsle
[ "$mipstype" = "0" ] && cpucore="mips_softfloat" || cpucore="mipsle_softfloat"
fi
echo $cpucore
@@ -1,7 +1,7 @@
#!/usr/bin/ucode
'use strict';
import { connect } from 'ubus';
import { popen, readfile, writefile, unlink } from 'fs';
import { popen, readfile, writefile, unlink, stat } from 'fs';
import { cursor } from 'uci';
function trim(s) {
@@ -39,7 +39,7 @@ function get_procd_status() {
return { running: false };
}
function get_rogue_pid(managed_pid) {
function get_real_lucky_pid() {
let p = popen('pidof /usr/bin/lucky', 'r');
if (!p)
return null;
@@ -51,32 +51,89 @@ function get_rogue_pid(managed_pid) {
return null;
let pids = split(trim(pid_str), /\s+/);
for (let i = 0; i < length(pids); i++) {
let pid = pids[i];
if (pid != '' && (managed_pid == null || pid != (managed_pid + '')))
return pid;
}
return null;
return (length(pids) > 0 && pids[0] != '') ? pids[0] : null;
}
function get_pid() {
let procd_st = get_procd_status();
let rogue_pid = get_rogue_pid(procd_st.pid);
return (procd_st.pid || rogue_pid || "") + "";
let real_pid = get_real_lucky_pid();
return (real_pid || procd_st.pid || "") + "";
}
// 获取配置文件的修改时间戳,用于热更新感知自愈
function get_config_mtime() {
let dir = get_configdir() || '/etc/config/lucky.daji';
let base_file = dir + '/lucky_base.lkcf';
let st = stat(base_file);
if (st && st.mtime)
return st.mtime;
let conf_file = dir + '/lucky.conf';
st = stat(conf_file);
if (st && st.mtime)
return st.mtime;
st = stat(dir);
return st ? (st.mtime || 0) : 0;
}
// 原生多级 fallback 架构探测,替代外部历史遗留脚本
function detect_arch() {
let u = connect();
let arch = '';
if (u) {
let board = u.call('system', 'board');
if (board && board.release && board.release.arch) {
arch = board.release.arch;
}
}
if (!arch) {
let rel = readfile('/etc/openwrt_release');
if (rel) {
let m = match(rel, /DISTRIB_ARCH=['"]?([^'"\n]+)/);
if (m) arch = m[1];
}
}
if (!arch) {
let p = popen('uname -m', 'r');
if (p) {
arch = trim(p.read('all'));
p.close();
}
}
if (!arch && u) {
let board = u.call('system', 'board');
if (board && board.release && board.release.target) {
arch = board.release.target;
}
}
arch = lc(arch || '');
if (match(arch, /x86_64|amd64/)) return 'x86_64';
if (match(arch, /i[3-6]86|x86/)) return 'i386';
if (match(arch, /aarch64|arm64|armv8/)) return 'arm64';
if (match(arch, /armv7/)) return 'armv7';
if (match(arch, /armv6/)) return 'armv6';
if (match(arch, /armv5/)) return 'armv5';
if (match(arch, /arm/)) return 'armv7';
if (match(arch, /mipsel/)) return 'mipsle_softfloat';
if (match(arch, /mips/)) return 'mips_softfloat';
return arch || 'unknown';
}
const CACHE_FILE = '/tmp/lucky_info_cache';
// 读取缓存,仅在 PID 未变(包括停止状态 pid=="")时返回有效数据
// 读取缓存:需同时满足 PID 未变且配置文件 mtime 未变
function get_cache() {
try {
let content = readfile(CACHE_FILE);
if (content) {
let cache = json(content);
let currentPid = get_pid();
if (cache && cache.pid === currentPid && cache.data) {
let currentMtime = get_config_mtime();
if (cache && cache.pid === currentPid && cache.mtime === currentMtime && cache.data) {
return cache.data;
}
}
@@ -84,11 +141,12 @@ function get_cache() {
return null;
}
// 写入缓存,绑定当前 PID
// 写入缓存,同时绑定 PID 与配置文件 mtime
function set_cache(data) {
let pid = get_pid();
let mtime = get_config_mtime();
if (data) {
writefile(CACHE_FILE, sprintf("%J", { pid: pid, data: data }));
writefile(CACHE_FILE, sprintf("%J", { pid: pid, mtime: mtime, data: data }));
}
}
@@ -105,16 +163,16 @@ const methods = {
// 1. 检测运行状态
let procd_st = get_procd_status();
let rogue_pid = get_rogue_pid(procd_st.pid);
let real_pid = get_real_lucky_pid();
let state = 'stopped';
if (procd_st.running)
state = 'managed';
else if (rogue_pid)
else if (real_pid)
state = 'unmanaged';
let running = (state !== 'stopped');
let pid = procd_st.pid || rogue_pid;
let pid = (state === 'managed' ? (real_pid || procd_st.pid) : real_pid) || null;
// 2. 程序信息(从缓存或实时获取)
let luckyInfo = "";
@@ -136,8 +194,7 @@ const methods = {
"/usr/bin/lucky -info");
if (fd) { luckyInfo = trim(fd.read('all')); fd.close(); }
fd = popen("/usr/bin/luckyarch");
if (fd) { luckyArch = trim(fd.read('all')); fd.close(); }
luckyArch = detect_arch();
let cmd = configPath ?
`/usr/bin/lucky -baseConfInfo -cd ${shellquote(configPath)}` :
@@ -182,20 +239,21 @@ const methods = {
get_status: {
call: function() {
let procd_st = get_procd_status();
let rogue_pid = get_rogue_pid(procd_st.pid);
let real_pid = get_real_lucky_pid();
let state = 'stopped';
if (procd_st.running)
state = 'managed';
else if (rogue_pid)
else if (real_pid)
state = 'unmanaged';
let running = (state !== 'stopped');
let pid = (state === 'managed' ? (real_pid || procd_st.pid) : real_pid) || null;
return {
running: running,
state: state,
pid: procd_st.pid || rogue_pid
pid: pid
};
}
},
@@ -212,7 +270,7 @@ const methods = {
args: { key: 'key', value: 'value' },
call: function(req) {
if (!req.args || !req.args.key) {
return { ret: 1 };
return { ret: 1, error: "Missing key" };
}
let key = req.args.key;
let value = req.args.value || "";
@@ -223,9 +281,30 @@ const methods = {
let realKey = key;
let cmd = "";
if (key === "admin_http_port") realKey = "AdminWebListenPort";
else if (key === "admin_safe_url") realKey = "SafeURL";
else if (key === "switch_Internetaccess") realKey = "AllowInternetaccess";
if (key === "admin_http_port") {
let portVal = trim("" + value);
if (!match(portVal, /^[1-9][0-9]{0,4}$/)) {
return { ret: 1, error: "Invalid port format" };
}
let p = int(portVal);
if (p < 1 || p > 65535) {
return { ret: 1, error: "Port out of range (1-65535)" };
}
realKey = "AdminWebListenPort";
value = "" + p;
} else if (key === "admin_safe_url") {
let urlVal = trim("" + value);
if (urlVal !== "") {
if (!match(urlVal, /^\/[-a-zA-Z0-9_\/]+$/) || match(urlVal, /\.\./)) {
return { ret: 1, error: "Invalid SafeURL path" };
}
}
realKey = "SafeURL";
value = urlVal;
} else if (key === "switch_Internetaccess") {
realKey = "AllowInternetaccess";
value = (value === true || value === "1" || value === "true") ? "true" : "false";
}
if (key === "admin_safe_url" && value === "") {
cmd = `/usr/bin/lucky -rCancelSafeURL`;
@@ -237,12 +316,25 @@ const methods = {
cmd += ` -cd ${shellquote(configPath)}`;
}
system(cmd);
clear_cache();
ret = 0;
} else if (key === "configdir") {
let p = trim("" + value);
if (!match(p, /^\/[-a-zA-Z0-9_.\/]+$/) || match(p, /\.\./)) {
return { ret: 1, error: "Invalid config directory path" };
}
// 拦截系统敏感目录
let forbidden = ['/', '/bin', '/sbin', '/etc', '/usr', '/lib', '/dev', '/proc', '/sys', '/root', '/var'];
for (let i = 0; i < length(forbidden); i++) {
if (p === forbidden[i] || p === forbidden[i] + '/') {
return { ret: 1, error: "System directory not allowed" };
}
}
let c = cursor();
c.load('lucky');
c.set('lucky', '@lucky[0]', 'configdir', value);
c.set('lucky', '@lucky[0]', 'configdir', p);
c.commit('lucky');
clear_cache();
ret = 0;
} else if (key === "reset_auth_info") {
let cmd1 = `/usr/bin/lucky -setconf -key AdminAccount -value 666`;
@@ -253,6 +345,7 @@ const methods = {
}
system(cmd1);
system(cmd2);
clear_cache();
ret = 0;
}
@@ -268,18 +361,11 @@ const methods = {
let action = req.args.action;
let applied = true;
if (action === "start" || action === "restart" || action === "stop") {
// 停止时清除缓存
if (action === "stop") {
clear_cache();
}
if (action === "restart") {
// 重启前,强制强杀所有可能残留的 lucky 野进程,确保重新由 procd 唯一受控拉起
let p = popen('killall -9 lucky', 'r');
if (p) p.close();
}
clear_cache();
// 进程控制权限完全交由 init 脚本处理,杜绝 ucode 越权强杀竞态
let rc = system(`/etc/init.d/lucky ${action} >/dev/null 2>&1`);
applied = (rc === 0);
system("sleep 2");
system("sleep 1");
}
return { success: true, applied: applied };
}
+2 -2
View File
@@ -3,11 +3,11 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-meow
# Keep in sync with meow/Makefile.
PKG_BASE_VERSION?=0.21.2
PKG_BASE_VERSION?=0.22.0
# Rewritten automatically by .github/workflows/sync-upstream.yml; the LuCI
# package always carries the same version as the core it drives.
PKG_VERSION?=$(PKG_BASE_VERSION).202610021113
PKG_VERSION?=$(PKG_BASE_VERSION).202610030155
PKG_RELEASE?=1
include $(INCLUDE_DIR)/package.mk
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box
PKG_VERSION:=0.1.278
PKG_RELEASE:=4
PKG_VERSION:=0.1.280
PKG_RELEASE:=5
PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later
+1 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-ssserver-next
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_MAINTAINER:=Dante <dante@example.com>
PKG_LICENSE:=Apache-2.0
@@ -6,7 +6,7 @@ import * as fs from 'fs';
const methods = {
get_log: {
call: function() {
let p = fs.popen('logread -e ssserver');
let p = fs.popen("logread -e 'ssserver' | grep -E 'ssserver(\\[[0-9]+\\])?: ' | tail -n 150");
let log = '';
if (p) {
log = p.read('all');
+3 -1
View File
@@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=lucky
PKG_VERSION:=2.27.2
PKG_RELEASE:=3
PKG_RELEASE:=4
PKGARCH:=all
LUCKY_CONF_DIR := /etc/config/lucky.daji
LUCKY_TMP_BACKUP := /tmp/luckyconfdir
@@ -82,9 +82,11 @@ endef
define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/usr/bin/
$(INSTALL_DIR) $(1)/usr/libexec/
$(INSTALL_DIR) $(1)/etc/init.d/
$(INSTALL_DIR) $(1)/etc/config/
$(INSTALL_BIN) $(PKG_BUILD_DIR)/lucky $(1)/usr/bin/lucky
$(INSTALL_BIN) ./files/lucky-runner $(1)/usr/libexec/lucky-runner
$(INSTALL_BIN) ./files/lucky.init $(1)/etc/init.d/lucky
$(INSTALL_CONF) $(CURDIR)/files/luckyuci $(1)/etc/config/lucky
endef
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# Copyright (C) 2026 Dante <dante@seckv.com>
#
# Smart process runner & supervisor for Lucky.
# Procd executes lucky-runner as a persistent managed supervisor.
# It cleans rogue orphans, starts lucky, and waits on it.
# When lucky self-restarts from Web UI (causing os.Exit(0) in parent),
# lucky-runner detects child exit, eliminates spawned rogue orphans,
# and restarts a fresh instance within the procd-supervised hierarchy.
orphan_pids=$(pidof /usr/bin/lucky)
[ -n "$orphan_pids" ] && kill -9 $orphan_pids >/dev/null 2>&1
while true; do
/usr/bin/lucky "$@" &
LUCKY_PID=$!
trap 'kill -TERM $LUCKY_PID 2>/dev/null; exit 0' TERM INT
wait $LUCKY_PID
EXIT_CODE=$?
usleep 200000 2>/dev/null || sleep 1
NEW_PID=$(pidof /usr/bin/lucky)
if [ -n "$NEW_PID" ]; then
kill -9 $NEW_PID >/dev/null 2>&1
usleep 200000 2>/dev/null || sleep 1
continue
fi
exit $EXIT_CODE
done
Regular → Executable
+40 -12
View File
@@ -1,15 +1,17 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2021-2024 daji <https://github.com/gdy666/lucky>
# Enhanced by Dante <dante@seckv.com>
START=99
USE_PROCD=1
PROG=/usr/bin/lucky
RUNNER=/usr/libexec/lucky-runner
DEFAULT_PRO_CONF='/etc/config/lucky.daji/'
lucky_instance() {
local cfg="$1"
local enabled configdir logger
local enabled configdir logger launcher
config_get_bool enabled "$cfg" enabled 0
config_get configdir "$cfg" configdir "$DEFAULT_PRO_CONF"
@@ -19,32 +21,58 @@ lucky_instance() {
[ -d "$configdir" ] || mkdir -p "$configdir" 2>/dev/null
launcher="$PROG"
[ -x "$RUNNER" ] && launcher="$RUNNER"
procd_open_instance "$cfg"
procd_set_param command "$PROG" -cd "$configdir"
procd_set_param respawn
procd_set_param command "$launcher" -cd "$configdir"
procd_set_param respawn 3600 3 0
procd_set_param stdout "$logger"
procd_set_param stderr "$logger"
procd_close_instance
}
start_service() {
# 强制清理可能脱离 procd 管控的独立进程(如通过 Lucky 面板自行更新/重启产生的孤儿进程)
local pids=$(pidof /usr/bin/lucky)
[ -n "$pids" ] && kill -9 $pids >/dev/null 2>&1 || true
# 检查并清理残留的脱管孤儿进程,确保端口完全释放
local pids=$(pidof "$PROG")
if [ -n "$pids" ]; then
kill -TERM $pids >/dev/null 2>&1
local timeout=20
while [ $timeout -gt 0 ]; do
kill -0 $pids >/dev/null 2>&1 || break
usleep 100000 2>/dev/null || sleep 1
timeout=$((timeout - 1))
done
local remaining=$(pidof "$PROG")
[ -n "$remaining" ] && kill -9 $remaining >/dev/null 2>&1 || true
fi
config_load lucky
config_foreach lucky_instance lucky
}
stop_service() {
local pids=$(pidof /usr/bin/lucky)
[ -n "$pids" ] && kill -9 $pids >/dev/null 2>&1 || true
local pids=$(pidof "$PROG")
[ -z "$pids" ] && return 0
kill -TERM $pids >/dev/null 2>&1
local timeout=20
while [ $timeout -gt 0 ]; do
kill -0 $pids >/dev/null 2>&1 || break
usleep 100000 2>/dev/null || sleep 1
timeout=$((timeout - 1))
done
# 兜底清理未退出的孤儿脱管进程
local remaining=$(pidof "$PROG")
[ -n "$remaining" ] && kill -9 $remaining >/dev/null 2>&1 || true
}
reload_service() {
stop
start
}
service_triggers() {
procd_add_reload_trigger "lucky"
}
reload_service() {
restart
}
+2 -2
View File
@@ -3,7 +3,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=meow
# Upstream release baseline. Bump by hand when meow-rs tags a new release.
PKG_BASE_VERSION?=0.21.2
PKG_BASE_VERSION?=0.22.0
# PKG_VERSION is rewritten automatically by
# .github/workflows/sync-upstream.yml from the pinned upstream commit's
@@ -11,7 +11,7 @@ PKG_BASE_VERSION?=0.21.2
# All-numeric dotted segments keep the version valid for both package
# managers: apk -> meow-0.21.2.202610010430-r1.apk
# ipk -> meow_0.21.2.202610010430-1_<arch>.ipk
PKG_VERSION?=$(PKG_BASE_VERSION).202610021113
PKG_VERSION?=$(PKG_BASE_VERSION).202610030155
PKG_RELEASE?=1
include $(INCLUDE_DIR)/package.mk
Binary file not shown.
+2 -2
View File
@@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=natflow
PKG_VERSION:=20260531
PKG_RELEASE:=86
PKG_VERSION:=20260930
PKG_RELEASE:=87
PKG_SOURCE:=$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
+3 -3
View File
@@ -2,12 +2,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=proot
PKG_VERSION:=5.4.1
PKG_RELEASE:=4
PKG_VERSION:=5.5.0
PKG_RELEASE:=5
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/proot-me/proot.git
PKG_SOURCE_VERSION:=d4b49e2243dee34e41c13dfeca6a3d07e9cf0f00
PKG_SOURCE_VERSION:=25dc6a3134891f98a79f57ce1c2c1b23ff15cad1
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/python/python3-version.mk
+5 -6
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=upx
PKG_VERSION:=5.2.1
PKG_RELEASE:=3
PKG_RELEASE:=4
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-src.tar.xz
PKG_SOURCE_URL:=https://github.com/upx/upx/releases/download/v$(PKG_VERSION)
@@ -20,7 +20,6 @@ PKG_LICENSE:=GPL-2.0-only
PKG_LICENSE_FILES:=COPYING LICENSE
HOST_BUILD_DIR:=$(BUILD_DIR_HOST)/$(PKG_NAME)-$(PKG_VERSION)-src
HOST_BUILD_DEPENDS:=ucl/host
MAKE_PATH:=src
include $(INCLUDE_DIR)/package.mk
@@ -29,7 +28,7 @@ include $(INCLUDE_DIR)/host-build.mk
define Package/upx
SECTION:=utils
CATEGORY:=Utilities
DEPENDS:=+libucl +libstdcpp +zlib
DEPENDS:=+libstdcpp
TITLE:=The Ultimate Packer for eXecutables
URL:=https://upx.github.io/
endef
@@ -40,14 +39,14 @@ executable packer for several executable formats.
endef
define Host/Compile
UPX_UCLDIR=$(STAGING_DIR_HOST) \
$(MAKE) -C $(HOST_BUILD_DIR)/src \
$(MAKE) -C $(HOST_BUILD_DIR) release \
LDFLAGS="$(HOST_LDFLAGS)" \
CXX="$(HOSTCXX)"
endef
define Host/Install
$(CP) $(HOST_BUILD_DIR)/build/release/upx $(STAGING_DIR_HOST)/bin/upx
$(INSTALL_DIR) $(STAGING_DIR_HOST)/bin
$(INSTALL_BIN) $(HOST_BUILD_DIR)/build/release/upx $(STAGING_DIR_HOST)/bin/upx
endef
define Host/Clean
+21 -6
View File
@@ -50,12 +50,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=wwand
PKG_VERSION:=1.6.9
PKG_RELEASE:=66
PKG_VERSION:=1.6.9_p2
PKG_RELEASE:=67
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
PKG_SOURCE_VERSION:=3a1fcc492e4c79b6ebee9189c3725300df65cc50
PKG_SOURCE_VERSION:=e82b67689936839a249ae7a69b9a67e070db253d
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only
@@ -641,14 +641,29 @@ endef
define Package/wwand-apntest/description
Runs the configured APN tests in sequence on a dedicated test box: selects the
SIM (physical slot or eUICC profile), dials each APN through wwand, runs the
configured checks and reports one verdict per test. Tests are grouped by SIM so
each slot switch or profile enable happens once per run.
SIM (physical slot), dials each APN through wwand and netifd, checks the address
pool and resolvers, pings, and reports one verdict per check to an NSCA monitor
(send_nsca, when `monitor` is configured). Tests are grouped by SIM so each slot
switch happens once per run. Scheduled from cron (globals.schedule).
endef
define Package/wwand-apntest/conffiles
/etc/config/apntest
endef
define Package/wwand-apntest/install
$(INSTALL_DIR) $(1)$(UCDIR)/apntest
$(INSTALL_DATA) $(WWAND_UCODE)/apntest/plan.uc $(1)$(UCDIR)/apntest/
$(INSTALL_DATA) $(WWAND_UCODE)/apntest/runner.uc $(1)$(UCDIR)/apntest/
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(WWAND_UCODE)/apntest_cli.uc $(1)/usr/sbin/wwand-apntest
# no daemon: the init script only keeps the cron line of the plan's
# schedule in root's crontab (start, reload_config, stop)
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(PKG_BUILD_DIR)/files/wwand-apntest.init $(1)/etc/init.d/wwand-apntest
# empty plan by default: nothing runs until a test is configured
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) $(PKG_BUILD_DIR)/files/apntest.config $(1)/etc/config/apntest
endef
$(eval $(call BuildPackage,wwand))