mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 18:52:51 +08:00
⛅ Sync 2026-09-16 03:46:54
This commit is contained in:
1 parent
b5b3006d79
commit
0deac8fcbb
29 files changed
+1215
-429
No files matched your search
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=0.1.41
|
||||
PKG_RELEASE:=16
|
||||
PKG_RELEASE:=17
|
||||
|
||||
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
|
||||
ifdef SOURCE_DATE_EPOCH
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
* GENERATED — do not edit. Edit fwlive.css and run: node scripts/embed-fwlive-css.js
|
||||
*/
|
||||
return baseclass.extend({
|
||||
styleText: "\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-display-bar-label {\n\tflex: 0 0 auto;\n\tfoLine truncated
|
||||
styleText: "\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-display-bar-label {\n\tflex: 0 0 auto;\n\tfoLine truncated
|
||||
});
|
||||
@@ -670,6 +670,25 @@
|
||||
color: var(--text-color-high);
|
||||
font-size: 0.92em;
|
||||
}
|
||||
.fwlive-summary {
|
||||
margin: 0 0 10px;
|
||||
padding: 8px 12px;
|
||||
background: var(--background-color-low);
|
||||
border: 1px solid var(--border-color-medium);
|
||||
border-radius: 3px;
|
||||
color: var(--text-color-high);
|
||||
}
|
||||
.fwlive-summary-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.fwlive-summary-body {
|
||||
margin: 8px 0 0;
|
||||
font: inherit;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.fwlive-tint-warn {
|
||||
display: none;
|
||||
color: var(--warn-color-high);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* callbacks - {} (unused; present for API consistency)
|
||||
*
|
||||
* renderRows(host, state, callbacks) → void
|
||||
* host - <tbody> element (cleared and rebuilt; element itself is kept)
|
||||
* host - <tbody> element (kept; normal polls reuse keyed rows)
|
||||
* state - shallow copy: { rows, columns, viewMode, messageLayout,
|
||||
* expandedRowId, rowTint, showHostnames,
|
||||
* hostnameCache, firewallBackend }
|
||||
@@ -24,8 +24,9 @@
|
||||
* Internals (not a second public contract): columnLabel, columnCellClass,
|
||||
* flowCell, buildColumnCell.
|
||||
*
|
||||
* Modules must not mutate state. host contents are cleared then rebuilt
|
||||
* (idempotent replace). Does not touch #fwlive-scroll or #fwlive-empty.
|
||||
* Modules must not mutate state. Forced renders clear and rebuild the host;
|
||||
* normal polls reuse unchanged keyed rows and only build changed rows. Does
|
||||
* not touch #fwlive-scroll or #fwlive-empty.
|
||||
*/
|
||||
|
||||
function columnLabel(col) {
|
||||
@@ -255,49 +256,146 @@ function renderThead(host, state, _callbacks) {
|
||||
}
|
||||
}
|
||||
|
||||
function rowRenderKey(row, state, columns) {
|
||||
return JSON.stringify([
|
||||
row.id,
|
||||
row.timestamp,
|
||||
row.action,
|
||||
row.rule_hint,
|
||||
row.rule_label,
|
||||
row.interface_in,
|
||||
row.interface_out,
|
||||
row.direction,
|
||||
row.proto,
|
||||
row.src,
|
||||
row.sport,
|
||||
row.dst,
|
||||
row.dport,
|
||||
row.flags,
|
||||
row.length,
|
||||
row.message,
|
||||
columns,
|
||||
state.viewMode,
|
||||
state.messageLayout,
|
||||
state.expandedRowId === row.id,
|
||||
!!state.rowTint,
|
||||
!!state.showHostnames,
|
||||
state.firewallBackend
|
||||
]);
|
||||
}
|
||||
|
||||
function rowClass(index, row, state, callbacks) {
|
||||
return [
|
||||
index % 2 ? 'fwlive-row-alt' : '',
|
||||
state.viewMode === 'simple' ? 'fwlive-row-clickable' : '',
|
||||
state.expandedRowId === row.id ? 'fwlive-row-expanded' : '',
|
||||
state.rowTint ? callbacks.actionRowTintClass(row.action) : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function buildRow(row, index, state, columns, callbacks) {
|
||||
const cells = [];
|
||||
for (let c = 0; c < columns.length; c++)
|
||||
cells.push(buildColumnCell(columns[c], row, state, callbacks));
|
||||
|
||||
const tr = E(
|
||||
'tr',
|
||||
{
|
||||
'class': rowClass(index, row, state, callbacks),
|
||||
'click': state.viewMode === 'simple' ? (ev) => callbacks.onRowClick(row.id, ev) : null
|
||||
},
|
||||
cells
|
||||
);
|
||||
tr._fwliveRowId = String(row.id);
|
||||
tr._fwliveRowKey = rowRenderKey(row, state, columns);
|
||||
return tr;
|
||||
}
|
||||
|
||||
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' }, [
|
||||
log.formatMessageDisplay(row.message, 'wrap') || '—'
|
||||
])
|
||||
])
|
||||
]);
|
||||
expansion._fwliveExpansionFor = String(row.id);
|
||||
expansion._fwliveExpansionKey = rowRenderKey(row, state, columns);
|
||||
return expansion;
|
||||
}
|
||||
|
||||
function renderAllRows(host, rows, state, columns, callbacks) {
|
||||
host.innerHTML = '';
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
host.appendChild(buildRow(row, i, state, columns, callbacks));
|
||||
if (state.viewMode === 'simple' && state.expandedRowId === row.id)
|
||||
host.appendChild(buildExpansionRow(row, state, columns));
|
||||
}
|
||||
}
|
||||
|
||||
function canReuseRows(host) {
|
||||
return (
|
||||
host &&
|
||||
host.childNodes &&
|
||||
typeof host.childNodes.length === 'number' &&
|
||||
typeof host.insertBefore === 'function' &&
|
||||
typeof host.removeChild === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
function renderRows(host, state, callbacks) {
|
||||
const rows = state.rows || [];
|
||||
const columns = state.columns || [];
|
||||
|
||||
host.innerHTML = '';
|
||||
if (state.forceRender || !canReuseRows(host)) {
|
||||
renderAllRows(host, rows, state, columns, callbacks);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingRows = new Map();
|
||||
const existingExpansions = new Map();
|
||||
const oldChildren = Array.prototype.slice.call(host.childNodes);
|
||||
for (let i = 0; i < oldChildren.length; i++) {
|
||||
const child = oldChildren[i];
|
||||
if (child._fwliveRowId != null) existingRows.set(child._fwliveRowId, child);
|
||||
else if (child._fwliveExpansionFor != null)
|
||||
existingExpansions.set(child._fwliveExpansionFor, child);
|
||||
}
|
||||
|
||||
const desired = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i];
|
||||
const rowClass = [
|
||||
i % 2 ? 'fwlive-row-alt' : '',
|
||||
state.viewMode === 'simple' ? 'fwlive-row-clickable' : '',
|
||||
state.expandedRowId === r.id ? 'fwlive-row-expanded' : '',
|
||||
state.rowTint ? callbacks.actionRowTintClass(r.action) : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const cells = [];
|
||||
for (let c = 0; c < columns.length; c++)
|
||||
cells.push(buildColumnCell(columns[c], r, state, callbacks));
|
||||
const row = rows[i];
|
||||
const id = String(row.id);
|
||||
const key = rowRenderKey(row, state, columns);
|
||||
let tr = existingRows.get(id);
|
||||
if (!tr || tr._fwliveRowKey !== key) tr = buildRow(row, i, state, columns, callbacks);
|
||||
else tr.setAttribute('class', rowClass(i, row, state, callbacks));
|
||||
tr._fwliveRowId = id;
|
||||
tr._fwliveRowKey = key;
|
||||
desired.push(tr);
|
||||
|
||||
const tr = E(
|
||||
'tr',
|
||||
{
|
||||
'class': rowClass,
|
||||
'click': state.viewMode === 'simple' ? (ev) => callbacks.onRowClick(r.id, ev) : null
|
||||
},
|
||||
cells
|
||||
);
|
||||
host.appendChild(tr);
|
||||
|
||||
if (state.viewMode === 'simple' && state.expandedRowId === r.id) {
|
||||
host.appendChild(
|
||||
E('tr', { 'class': 'fwlive-msg-expand' }, [
|
||||
E('td', { 'colspan': String(columns.length) }, [
|
||||
E('div', { 'class': 'fwlive-msg-expand-label' }, [_('Message')]),
|
||||
E('pre', { 'class': 'fwlive-msg-expand-body' }, [
|
||||
log.formatMessageDisplay(r.message, 'wrap') || '—'
|
||||
])
|
||||
])
|
||||
])
|
||||
);
|
||||
if (state.viewMode === 'simple' && state.expandedRowId === row.id) {
|
||||
let expansion = existingExpansions.get(id);
|
||||
if (!expansion || expansion._fwliveExpansionKey !== key)
|
||||
expansion = buildExpansionRow(row, state, columns);
|
||||
desired.push(expansion);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < desired.length; i++) {
|
||||
const current = host.childNodes[i] || null;
|
||||
if (current !== desired[i]) host.insertBefore(desired[i], current);
|
||||
}
|
||||
|
||||
const used = new Set(desired);
|
||||
const currentChildren = Array.prototype.slice.call(host.childNodes);
|
||||
for (let i = 0; i < currentChildren.length; i++) {
|
||||
if (!used.has(currentChildren[i])) host.removeChild(currentChildren[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
|
||||
@@ -102,6 +102,12 @@ return view.extend({
|
||||
resumeMerge: false,
|
||||
pollFn: null,
|
||||
pollDataInFlight: false,
|
||||
/* Every refresh trigger goes through this coordinator. A request made
|
||||
* while one is active is coalesced into one follow-up, never overlapped. */
|
||||
pollRequestPromise: null,
|
||||
pollRequestQueued: false,
|
||||
pollRequestWaiters: [],
|
||||
pollRequestQueuedWaiters: [],
|
||||
/* Layer 2 — visibility / RTT cadence / shed surfacing. */
|
||||
pollEpoch: 0,
|
||||
pollCadenceSec: 1,
|
||||
@@ -111,6 +117,10 @@ return view.extend({
|
||||
serverTruncated: 0,
|
||||
serverShed: null,
|
||||
degradedSampling: false,
|
||||
/* Layer 2 summary fallback — rows remain available behind an explicit toggle. */
|
||||
summaryMode: false,
|
||||
summaryRowsShown: false,
|
||||
summaryData: null,
|
||||
resolveLoadShed: false,
|
||||
visibilityBound: false,
|
||||
renderRaf: 0,
|
||||
@@ -769,6 +779,14 @@ return view.extend({
|
||||
|
||||
this.notePollRtt(rtt, errored);
|
||||
this.updateAdaptiveBanner();
|
||||
if (this.clientBackoffEnabled() && rtt > constants.POLL_RTT_SLOW_MS) {
|
||||
if (!this.summaryMode) this.enterSummaryMode(reply.summary);
|
||||
else {
|
||||
this.summaryData =
|
||||
reply.summary && typeof reply.summary === 'object' ? reply.summary : null;
|
||||
this.renderSummary();
|
||||
}
|
||||
}
|
||||
|
||||
const batch = this.normalizePollBatch(raw);
|
||||
this.lastPollNewEvents = batch.pollNew;
|
||||
@@ -884,6 +902,7 @@ return view.extend({
|
||||
|
||||
notePollRtt(ms, errored) {
|
||||
if (!this.clientBackoffEnabled()) {
|
||||
if (this.summaryMode) this.leaveSummaryMode();
|
||||
this.degradedSampling = false;
|
||||
/* Drop any partial streak so a slow sample from before the
|
||||
* adaptive:0 window cannot trip degraded on re-enable. */
|
||||
@@ -905,6 +924,7 @@ return view.extend({
|
||||
const cadence = this.cadenceForKind(kind);
|
||||
this.degradedSampling = cadence === constants.POLL_CADENCE_SLOW_S;
|
||||
if (cadence !== this.pollCadenceSec) this.setPollCadence(cadence);
|
||||
if (kind === 'fast' && this.summaryMode) this.leaveSummaryMode();
|
||||
},
|
||||
|
||||
stopPollingForHidden() {
|
||||
@@ -919,11 +939,12 @@ return view.extend({
|
||||
resumePollingAfterVisible() {
|
||||
this.bumpPollEpoch();
|
||||
this.resetRttHistory();
|
||||
/* In-flight poll from the hidden epoch must not apply after catch-up. */
|
||||
this.pollDataInFlight = false;
|
||||
if (!this.pollFn) this.pollFn = this.pollData.bind(this);
|
||||
this.setPollCadence(this.pollCadenceSec || constants.POLL_CADENCE_FAST_S);
|
||||
this.pollData();
|
||||
/* An old hidden-epoch request may still be on the wire. Queue the
|
||||
* catch-up behind it; stale-reply checks discard its application, while
|
||||
* the coordinator prevents a second server request from overlapping. */
|
||||
this.requestPoll();
|
||||
},
|
||||
|
||||
onVisibilityChange() {
|
||||
@@ -992,6 +1013,93 @@ return view.extend({
|
||||
}
|
||||
},
|
||||
|
||||
summaryListText(label, values) {
|
||||
const lines = [label + ':'];
|
||||
if (!Array.isArray(values) || !values.length) {
|
||||
lines.push(_('none'));
|
||||
return lines;
|
||||
}
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const item = values[i];
|
||||
if (!item || typeof item.value !== 'string') continue;
|
||||
const count = Number.isFinite(Number(item.count)) ? Number(item.count) : 0;
|
||||
lines.push('%s — %d'.format(item.value, count));
|
||||
}
|
||||
if (lines.length === 1) lines.push(_('none'));
|
||||
return lines;
|
||||
},
|
||||
|
||||
renderSummary() {
|
||||
const card = document.getElementById('fwlive-summary');
|
||||
const body = document.getElementById('fwlive-summary-body');
|
||||
if (!card || !body) return;
|
||||
const summary = this.summaryData;
|
||||
const lines = [_('Showing a compact summary of the top of the shown sample.')];
|
||||
if (!summary || typeof summary !== 'object') {
|
||||
lines.push(_('Summary data is unavailable; show rows to inspect the sample.'));
|
||||
} else if (summary.truncated) {
|
||||
lines.push(_('Summary was bounded before all categories could be included.'));
|
||||
} else {
|
||||
this.summaryListText(_('Top talkers'), summary.top_talkers).forEach((line) =>
|
||||
lines.push(line)
|
||||
);
|
||||
this.summaryListText(_('Top drops'), summary.top_drops).forEach((line) =>
|
||||
lines.push(line)
|
||||
);
|
||||
this.summaryListText(_('Top rules'), summary.top_rules).forEach((line) =>
|
||||
lines.push(line)
|
||||
);
|
||||
}
|
||||
body.textContent = lines.join('\n');
|
||||
this.updateSummaryUi();
|
||||
},
|
||||
|
||||
updateSummaryUi() {
|
||||
const card = document.getElementById('fwlive-summary');
|
||||
const scroll = document.getElementById('fwlive-scroll');
|
||||
const empty = document.getElementById('fwlive-empty');
|
||||
const toggle = document.getElementById('fwlive-summary-rows');
|
||||
if (card) {
|
||||
if (!card.style) card.style = { display: '' };
|
||||
card.style.display = this.summaryMode ? 'block' : 'none';
|
||||
}
|
||||
if (scroll) {
|
||||
if (!scroll.style) scroll.style = { display: '' };
|
||||
scroll.style.display = this.summaryMode && !this.summaryRowsShown ? 'none' : '';
|
||||
}
|
||||
if (empty) {
|
||||
if (!empty.style) empty.style = { display: '' };
|
||||
empty.style.display = this.summaryMode && !this.summaryRowsShown ? 'none' : '';
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.textContent = this.summaryRowsShown ? _('Hide rows') : _('Show rows');
|
||||
toggle.setAttribute('aria-pressed', this.summaryRowsShown ? 'true' : 'false');
|
||||
}
|
||||
},
|
||||
|
||||
enterSummaryMode(summary) {
|
||||
this.summaryMode = true;
|
||||
this.summaryRowsShown = false;
|
||||
this.summaryData = summary && typeof summary === 'object' ? summary : null;
|
||||
this.renderSummary();
|
||||
},
|
||||
|
||||
leaveSummaryMode() {
|
||||
if (!this.summaryMode) return;
|
||||
this.summaryMode = false;
|
||||
this.summaryRowsShown = false;
|
||||
this.summaryData = null;
|
||||
this.updateSummaryUi();
|
||||
this.renderRows(true);
|
||||
},
|
||||
|
||||
onSummaryRowsToggle() {
|
||||
if (!this.summaryMode) return;
|
||||
this.summaryRowsShown = !this.summaryRowsShown;
|
||||
this.updateSummaryUi();
|
||||
if (this.summaryRowsShown) this.renderRows(true);
|
||||
},
|
||||
|
||||
scheduleRenderRows(force) {
|
||||
const doForce = !!force;
|
||||
const epoch = this.pollEpoch;
|
||||
@@ -1193,7 +1301,7 @@ return view.extend({
|
||||
if (!wasPaused && this.paused) {
|
||||
this.pauseBufferLoading = true;
|
||||
this.updateStatus();
|
||||
this.fetchEntries()
|
||||
this.requestPoll()
|
||||
.catch(function () {})
|
||||
.finally(
|
||||
function () {
|
||||
@@ -1209,7 +1317,7 @@ return view.extend({
|
||||
/* Merge pause buffer with the first live poll — do not replace. */
|
||||
this.resumeMerge = true;
|
||||
const epoch = this.pollEpoch;
|
||||
this.fetchEntries()
|
||||
this.requestPoll()
|
||||
.then(() => {
|
||||
/* A hide/show bump abandons this epoch; the catch-up poll paints. */
|
||||
if (epoch === this.pollEpoch) this.renderRows(true);
|
||||
@@ -1232,7 +1340,7 @@ return view.extend({
|
||||
if (!this.paused) this.renderRows(true);
|
||||
else this.updateStatus();
|
||||
const epoch = this.pollEpoch;
|
||||
this.fetchEntries()
|
||||
this.requestPoll()
|
||||
.then(() => {
|
||||
/* A hide/show bump abandons this epoch; the catch-up poll paints. */
|
||||
if (epoch !== this.pollEpoch) return;
|
||||
@@ -1533,6 +1641,7 @@ return view.extend({
|
||||
{
|
||||
rows: rows.slice(),
|
||||
columns: this.activeColumns().slice(),
|
||||
forceRender: !!force,
|
||||
viewMode: this.viewMode,
|
||||
messageLayout: this.messageLayout,
|
||||
expandedRowId: this.expandedRowId,
|
||||
@@ -1641,14 +1750,82 @@ return view.extend({
|
||||
|
||||
const tintSel = document.getElementById('fwlive-row-tint');
|
||||
if (tintSel) tintSel.addEventListener('change', this.onRowTintPaletteChange.bind(this));
|
||||
|
||||
const summaryRows = document.getElementById('fwlive-summary-rows');
|
||||
if (summaryRows) summaryRows.addEventListener('click', this.onSummaryRowsToggle.bind(this));
|
||||
},
|
||||
|
||||
async pollData() {
|
||||
if (this.isTabHidden()) return;
|
||||
if (this.pollDataInFlight) return;
|
||||
return this.requestPoll();
|
||||
},
|
||||
|
||||
requestPoll() {
|
||||
if (this.isTabHidden()) return Promise.resolve();
|
||||
|
||||
const waiter = {};
|
||||
const promise = new Promise(function (resolve) {
|
||||
waiter.resolve = resolve;
|
||||
});
|
||||
|
||||
if (this.pollRequestPromise) {
|
||||
/* Keep one pending refresh intent. The latest view state is read when
|
||||
* that follow-up begins, so Pause/Resume/Limit changes coalesce safely. */
|
||||
this.pollRequestQueued = true;
|
||||
this.pollRequestQueuedWaiters.push(waiter);
|
||||
return promise;
|
||||
}
|
||||
|
||||
if (this.pollRequestQueued) {
|
||||
this.pollRequestQueuedWaiters.push(waiter);
|
||||
this.startPollRequest(this.pollRequestQueuedWaiters);
|
||||
this.pollRequestQueuedWaiters = [];
|
||||
this.pollRequestQueued = false;
|
||||
return promise;
|
||||
}
|
||||
|
||||
this.startPollRequest([waiter]);
|
||||
return promise;
|
||||
},
|
||||
|
||||
startPollRequest(waiters) {
|
||||
const epoch = this.pollEpoch;
|
||||
this.pollDataInFlight = true;
|
||||
const run = this.runPollRequest(epoch);
|
||||
this.pollRequestPromise = run;
|
||||
this.pollRequestWaiters = waiters;
|
||||
run.then(
|
||||
function (value) {
|
||||
this.finishPollRequest(run, value);
|
||||
}.bind(this),
|
||||
function () {
|
||||
/* runPollRequest normally absorbs local failures so the poll loop
|
||||
* remains alive; settle waiters even if a future change rejects. */
|
||||
this.finishPollRequest(run);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
finishPollRequest(run, value) {
|
||||
if (this.pollRequestPromise !== run) return;
|
||||
const waiters = this.pollRequestWaiters;
|
||||
this.pollRequestWaiters = [];
|
||||
this.pollRequestPromise = null;
|
||||
this.pollDataInFlight = false;
|
||||
for (let i = 0; i < waiters.length; i++) {
|
||||
waiters[i].resolve(value);
|
||||
}
|
||||
|
||||
/* If visibility changed while the request was active, retain the
|
||||
* queued intent until the visible catch-up can start it. */
|
||||
if (this.pollRequestQueued && !this.isTabHidden()) {
|
||||
const queued = this.pollRequestQueuedWaiters;
|
||||
this.pollRequestQueuedWaiters = [];
|
||||
this.pollRequestQueued = false;
|
||||
this.startPollRequest(queued);
|
||||
}
|
||||
},
|
||||
|
||||
async runPollRequest(epoch) {
|
||||
try {
|
||||
try {
|
||||
await this.fetchEntries();
|
||||
@@ -1661,7 +1838,11 @@ return view.extend({
|
||||
|
||||
if (epoch !== this.pollEpoch) return;
|
||||
|
||||
/* Pause freezes row rendering but polling remains active for health and
|
||||
* cadence state; summary mode can therefore appear while rows are paused
|
||||
* and stays behind the explicit Show rows control. */
|
||||
if (this.paused) this.updateStatus();
|
||||
else if (this.summaryMode) this.renderSummary();
|
||||
else this.scheduleRenderRows(!!this.pendingForceRender);
|
||||
|
||||
try {
|
||||
@@ -1669,11 +1850,10 @@ return view.extend({
|
||||
} catch (e) {
|
||||
/* resolve unavailable — show IPs */
|
||||
}
|
||||
} finally {
|
||||
/* Only the owning epoch clears — a stale hidden-era poll must not
|
||||
* drop the catch-up guard after resume. Resume clears
|
||||
* explicitly before starting the catch-up poll. */
|
||||
if (epoch === this.pollEpoch) this.pollDataInFlight = false;
|
||||
} catch (e) {
|
||||
/* Keep the coordinator promise settling so a queued refresh cannot
|
||||
* be stranded by an unexpected local rendering failure. */
|
||||
this.lastPollError = true;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1706,7 +1886,7 @@ return view.extend({
|
||||
}
|
||||
}
|
||||
return Promise.all([this.loadRulesMap(), this.loadLoggingStatus()]).then(() =>
|
||||
this.fetchEntries()
|
||||
this.requestPoll()
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1842,6 +2022,28 @@ return view.extend({
|
||||
]),
|
||||
E('div', { 'id': 'fwlive-flood', 'class': 'fwlive-flood' }, ['']),
|
||||
E('div', { 'id': 'fwlive-adaptive', 'class': 'fwlive-adaptive' }, ['']),
|
||||
E(
|
||||
'div',
|
||||
{ 'id': 'fwlive-summary', 'class': 'fwlive-summary', 'style': 'display:none' },
|
||||
[
|
||||
E('div', { 'class': 'fwlive-summary-head' }, [
|
||||
E('strong', {}, [_('Summary mode')]),
|
||||
E(
|
||||
'button',
|
||||
{
|
||||
'id': 'fwlive-summary-rows',
|
||||
'class': 'cbi-button fwlive-btn-ghost',
|
||||
'type': 'button',
|
||||
'aria-pressed': 'false'
|
||||
},
|
||||
[_('Show rows')]
|
||||
)
|
||||
]),
|
||||
E('pre', { 'id': 'fwlive-summary-body', 'class': 'fwlive-summary-body' }, [
|
||||
''
|
||||
])
|
||||
]
|
||||
),
|
||||
E('div', { 'id': 'fwlive-display-drawer', 'class': 'fwlive-display-bar' }, [
|
||||
E('span', { 'class': 'fwlive-display-bar-label' }, [_('Display options')]),
|
||||
E('div', { 'class': 'fwlive-display-controls' }, [
|
||||
|
||||
@@ -744,3 +744,33 @@ msgstr "mit iptables"
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89
|
||||
msgid "· %s"
|
||||
msgstr "· %s"
|
||||
|
||||
msgid "Hide rows"
|
||||
msgstr "Zeilen ausblenden"
|
||||
|
||||
msgid "none"
|
||||
msgstr "keine"
|
||||
|
||||
msgid "Show rows"
|
||||
msgstr "Zeilen anzeigen"
|
||||
|
||||
msgid "Showing a compact summary of the top of the shown sample."
|
||||
msgstr "Eine kompakte Zusammenfassung des Anfangs der angezeigten Stichprobe."
|
||||
|
||||
msgid "Summary data is unavailable; show rows to inspect the sample."
|
||||
msgstr "Zusammenfassungsdaten nicht verfügbar; Zeilen anzeigen, um die Stichprobe zu prüfen."
|
||||
|
||||
msgid "Summary mode"
|
||||
msgstr "Zusammenfassungsmodus"
|
||||
|
||||
msgid "Summary was bounded before all categories could be included."
|
||||
msgstr "Die Zusammenfassung wurde begrenzt, bevor alle Kategorien aufgenommen werden konnten."
|
||||
|
||||
msgid "Top drops"
|
||||
msgstr "Häufigste Drops"
|
||||
|
||||
msgid "Top rules"
|
||||
msgstr "Häufigste Regeln"
|
||||
|
||||
msgid "Top talkers"
|
||||
msgstr "Häufigste Absender"
|
||||
@@ -749,3 +749,33 @@ msgstr "иÑполÑзÑеÑÑÑ iptables"
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89
|
||||
msgid "· %s"
|
||||
msgstr "· %s"
|
||||
|
||||
msgid "Hide rows"
|
||||
msgstr "Скрыть строки"
|
||||
|
||||
msgid "none"
|
||||
msgstr "нет"
|
||||
|
||||
msgid "Show rows"
|
||||
msgstr "Показать строки"
|
||||
|
||||
msgid "Showing a compact summary of the top of the shown sample."
|
||||
msgstr "Показана краткая сводка верхней части отображаемой выборки."
|
||||
|
||||
msgid "Summary data is unavailable; show rows to inspect the sample."
|
||||
msgstr "Данные сводки недоступны; покажите строки для проверки выборки."
|
||||
|
||||
msgid "Summary mode"
|
||||
msgstr "Режим сводки"
|
||||
|
||||
msgid "Summary was bounded before all categories could be included."
|
||||
msgstr "Сводка ограничена до включения всех категорий."
|
||||
|
||||
msgid "Top drops"
|
||||
msgstr "Частые сбросы"
|
||||
|
||||
msgid "Top rules"
|
||||
msgstr "Частые правила"
|
||||
|
||||
msgid "Top talkers"
|
||||
msgstr "Частые источники"
|
||||
@@ -678,3 +678,44 @@ msgstr ""
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89
|
||||
msgid "· %s"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1063
|
||||
msgid "Hide rows"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1012
|
||||
msgid "none"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1039
|
||||
msgid "Top drops"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1042
|
||||
msgid "Top rules"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1036
|
||||
msgid "Top talkers"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1030
|
||||
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:1032
|
||||
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:1950
|
||||
msgid "Summary mode"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1034
|
||||
msgid "Summary was bounded before all categories could be included."
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1063
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1959
|
||||
msgid "Show rows"
|
||||
msgstr ""
|
||||
@@ -713,3 +713,33 @@ msgstr "ä½¿ç¨ iptables"
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:89
|
||||
msgid "· %s"
|
||||
msgstr "· %s"
|
||||
|
||||
msgid "Hide rows"
|
||||
msgstr "隐藏行"
|
||||
|
||||
msgid "none"
|
||||
msgstr "无"
|
||||
|
||||
msgid "Show rows"
|
||||
msgstr "显示行"
|
||||
|
||||
msgid "Showing a compact summary of the top of the shown sample."
|
||||
msgstr "显示当前样本顶部的简要摘要。"
|
||||
|
||||
msgid "Summary data is unavailable; show rows to inspect the sample."
|
||||
msgstr "摘要数据不可用;显示行以检查样本。"
|
||||
|
||||
msgid "Summary mode"
|
||||
msgstr "摘要模式"
|
||||
|
||||
msgid "Summary was bounded before all categories could be included."
|
||||
msgstr "摘要已受限,无法包含所有类别。"
|
||||
|
||||
msgid "Top drops"
|
||||
msgstr "主要丢弃"
|
||||
|
||||
msgid "Top rules"
|
||||
msgstr "主要规则"
|
||||
|
||||
msgid "Top talkers"
|
||||
msgstr "主要通信方"
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2025-2026 Lucas Albers <lucas.b.albers@gmail.com>
|
||||
#
|
||||
# Layer 1 adaptive poll cap. Sourced by rpcd/fwlive.
|
||||
# Layer 1 adaptive poll cap (#306). Sourced by rpcd/fwlive.
|
||||
# Always on unless test/triage override (no UCI / no product config):
|
||||
# FWLIVE_ADAPTIVE=0|false|off|no
|
||||
# or sentinel ${FWLIVE_ADAPTIVE_OFF_FILE:-<state-dir>/fwlive-adaptive-off}
|
||||
@@ -18,10 +18,10 @@
|
||||
# flock, lock busy, or corrupt state. Lock-busy ⇒ unlocked last-writer-wins is
|
||||
# acceptable (state stays one valid JSON line; ordering is not guaranteed).
|
||||
# Failed ubus log.read must NOT call record() — a ~0 ms failure is not "cold"
|
||||
# health and must not clear an existing hot/shed cap.
|
||||
# health and must not clear an existing hot/shed cap (#329 Hermes Q1).
|
||||
# Outside the measured duration interval: plan (pre), record/merge (post).
|
||||
# messages_received is 0 in Layer 1 — do not ash-scan the filter JSON after
|
||||
# end_cs; accurate count belongs in the filter or a later layer.
|
||||
# messages_received is supplied by the filter-side jsonfilter enumeration;
|
||||
# failed reads still use 0 without scanning the response in ash.
|
||||
|
||||
FWLIVE_ADAPTIVE_STATE_FILE="${FWLIVE_ADAPTIVE_STATE_FILE:-/var/run/fwlive-state.json}"
|
||||
# Optional overrides; when unset, lock/off paths are siblings of the current state.
|
||||
@@ -37,6 +37,7 @@ FWLIVE_ADAPTIVE_HOT_FLOOR=250
|
||||
FWLIVE_ADAPTIVE_WARM_MIN=50
|
||||
FWLIVE_ADAPTIVE_COOLDOWN_WARM_CS=300
|
||||
FWLIVE_ADAPTIVE_COOLDOWN_HOT_CS=600
|
||||
FWLIVE_ADAPTIVE_COOLDOWN_PROBE_CS=300
|
||||
|
||||
fwlive_adaptive_lock_path() {
|
||||
if [ -n "${FWLIVE_ADAPTIVE_LOCK_FILE:-}" ]; then
|
||||
@@ -273,7 +274,8 @@ fwlive_adaptive_bucket_for_ms() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Args: requested_limit duration_ms prev_limit prev_bucket prev_warm prev_completed_cs
|
||||
# Args: requested_limit duration_ms prev_limit prev_bucket prev_warm
|
||||
# prev_completed_cs planning
|
||||
# Prints next limit.
|
||||
fwlive_adaptive_compute_limit() {
|
||||
_req=$1
|
||||
@@ -282,6 +284,7 @@ fwlive_adaptive_compute_limit() {
|
||||
_prev_b=$4
|
||||
_prev_w=$5
|
||||
_prev_c=$6
|
||||
_planning=${7:-0}
|
||||
_max="${POLL_LINES_MAX:-2000}"
|
||||
_now=$(fwlive_adaptive_clock_cs)
|
||||
_bucket=$(fwlive_adaptive_bucket_for_ms "$_ms")
|
||||
@@ -292,9 +295,10 @@ fwlive_adaptive_compute_limit() {
|
||||
hot) _cd=$FWLIVE_ADAPTIVE_COOLDOWN_HOT_CS ;;
|
||||
warm) _cd=$FWLIVE_ADAPTIVE_COOLDOWN_WARM_CS ;;
|
||||
esac
|
||||
_elapsed=$((_now - _prev_c))
|
||||
|
||||
if [ "$_cd" -gt 0 ] && [ "$_prev_c" -gt 0 ] && \
|
||||
[ "$((_now - _prev_c))" -lt "$_cd" ]; then
|
||||
[ "$_elapsed" -lt "$_cd" ]; then
|
||||
if [ "$_ms" -le "$FWLIVE_ADAPTIVE_HOT_EXIT_MS" ] && \
|
||||
[ "$_bucket" != hot ]; then
|
||||
:
|
||||
@@ -308,9 +312,50 @@ fwlive_adaptive_compute_limit() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Once a hot/warm cooldown expires, deliberately test a larger request.
|
||||
# Planning and recording use the same pure helper: planning raises the next
|
||||
# request, while recording raises the retained limit only when that probe
|
||||
# completed below the hot-exit threshold. A still-hot probe therefore falls
|
||||
# through to the hot floor below.
|
||||
if [ "$_cd" -gt 0 ] && [ "$_prev_c" -gt 0 ] && \
|
||||
[ "$_elapsed" -ge "$_cd" ]; then
|
||||
if [ "$_planning" = 1 ]; then
|
||||
_limit=$_prev_l
|
||||
[ "$_limit" -lt 1 ] && _limit=$_max
|
||||
_limit=$((_limit * 2))
|
||||
[ "$_limit" -gt "$_max" ] && _limit=$_max
|
||||
[ "$_limit" -gt "$_req" ] && _limit=$_req
|
||||
[ "$_limit" -lt 1 ] && _limit=1
|
||||
printf '%s\n' "$_limit"
|
||||
return 0
|
||||
fi
|
||||
if [ "$_ms" -le "$FWLIVE_ADAPTIVE_HOT_EXIT_MS" ] && \
|
||||
[ "$_bucket" != hot ]; then
|
||||
_limit=$_prev_l
|
||||
[ "$_limit" -lt 1 ] && _limit=$_max
|
||||
_limit=$((_limit * 2))
|
||||
[ "$_limit" -gt "$_max" ] && _limit=$_max
|
||||
[ "$_limit" -gt "$_req" ] && _limit=$_req
|
||||
[ "$_limit" -lt 1 ] && _limit=1
|
||||
printf '%s\n' "$_limit"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$_bucket" in
|
||||
cold) _limit=$_max ;;
|
||||
cool) _limit=$FWLIVE_ADAPTIVE_COOL_CAP ;;
|
||||
cool)
|
||||
# A healthy cooldown probe may retain its raised limit for the
|
||||
# short probe window. Ordinary cool samples still use the fixed
|
||||
# 250-line cap.
|
||||
if [ "$_prev_l" -gt "$FWLIVE_ADAPTIVE_COOL_CAP" ] && \
|
||||
[ "$_prev_c" -gt 0 ] && \
|
||||
[ "$((_now - _prev_c))" -lt "$FWLIVE_ADAPTIVE_COOLDOWN_PROBE_CS" ]; then
|
||||
_limit=$_prev_l
|
||||
else
|
||||
_limit=$FWLIVE_ADAPTIVE_COOL_CAP
|
||||
fi
|
||||
;;
|
||||
warm)
|
||||
if [ "$_prev_w" = 1 ] && [ "$_prev_b" = warm ]; then
|
||||
_limit=$_prev_l
|
||||
@@ -350,7 +395,7 @@ fwlive_adaptive_plan() {
|
||||
_prev_w=$4
|
||||
_s=$5
|
||||
_prev_c=$6
|
||||
_l=$(fwlive_adaptive_compute_limit "$_req" "$_d" "$_prev_l" "$_b" "$_prev_w" "$_prev_c")
|
||||
_l=$(fwlive_adaptive_compute_limit "$_req" "$_d" "$_prev_l" "$_b" "$_prev_w" "$_prev_c" 1)
|
||||
_nb=$(fwlive_adaptive_bucket_for_ms "$_d")
|
||||
_shed=0
|
||||
[ "$_nb" = hot ] && _shed=1
|
||||
@@ -395,7 +440,9 @@ fwlive_adaptive_is_hot() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Merge adaptive siblings into a JSON object ending with }.
|
||||
# Merge adaptive siblings into a JSON object ending with }. A successful
|
||||
# filter already carries the exact messages_received count; do not append a
|
||||
# duplicate key in that case.
|
||||
# Args: json_body shed_flag limit truncated messages_received
|
||||
fwlive_adaptive_merge_reply() {
|
||||
_body=$1
|
||||
@@ -410,34 +457,32 @@ fwlive_adaptive_merge_reply() {
|
||||
*) printf '%s' "$_body"; return 0 ;;
|
||||
esac
|
||||
_base=${_body%\}}
|
||||
_has_msgs=0
|
||||
case "$_body" in
|
||||
*',"messages_received":'*) _has_msgs=1 ;;
|
||||
esac
|
||||
if [ "$_adapt" = 0 ]; then
|
||||
printf '%s,"adaptive":0,"messages_received":%s}' "$_base" "$_msgs"
|
||||
if [ "$_has_msgs" = 1 ]; then
|
||||
printf '%s,"adaptive":0}' "$_base"
|
||||
else
|
||||
printf '%s,"adaptive":0,"messages_received":%s}' "$_base" "$_msgs"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [ "$_shed" = 1 ]; then
|
||||
printf '%s,"adaptive":1,"messages_received":%s,"truncated":%s,"shed":{"level":"hot","limit":%s}}' \
|
||||
"$_base" "$_msgs" "$_trunc" "$_limit"
|
||||
if [ "$_has_msgs" = 1 ]; then
|
||||
printf '%s,"adaptive":1,"truncated":%s,"shed":{"level":"hot","limit":%s}}' \
|
||||
"$_base" "$_trunc" "$_limit"
|
||||
else
|
||||
printf '%s,"adaptive":1,"messages_received":%s,"truncated":%s,"shed":{"level":"hot","limit":%s}}' \
|
||||
"$_base" "$_msgs" "$_trunc" "$_limit"
|
||||
fi
|
||||
else
|
||||
printf '%s,"adaptive":1,"messages_received":%s,"truncated":%s}' \
|
||||
"$_base" "$_msgs" "$_trunc"
|
||||
if [ "$_has_msgs" = 1 ]; then
|
||||
printf '%s,"adaptive":1,"truncated":%s}' "$_base" "$_trunc"
|
||||
else
|
||||
printf '%s,"adaptive":1,"messages_received":%s,"truncated":%s}' \
|
||||
"$_base" "$_msgs" "$_trunc"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Count log[*] via "msg" keys — helper for tests / future filter-side count.
|
||||
# NOT called on the poll hot path: ash-scanning a 2000-entry
|
||||
# reply after end_cs is unmeasured overhead that cannot shed itself.
|
||||
fwlive_adaptive_count_log() {
|
||||
_json=$1
|
||||
_n=0
|
||||
_rest=$_json
|
||||
while :; do
|
||||
case "$_rest" in
|
||||
*'"msg"'*)
|
||||
_n=$((_n + 1))
|
||||
_rest=${_rest#*\"msg\"}
|
||||
;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
printf '%s\n' "$_n"
|
||||
}
|
||||
@@ -106,11 +106,101 @@ function is_fw(s, action) {
|
||||
if (has_hint(s) && (has_kv(s, "IN") || has_kv(s, "OUT") || has_kv(s, "SRC") || has_kv(s, "DST") || has_kv(s, "PROTO"))) return 1
|
||||
return 0
|
||||
}
|
||||
BEGIN { if (MODE != "json") ORS = "" }
|
||||
function utf8_prefix(s, max_bytes, out, i, c, width) {
|
||||
out = ""
|
||||
i = 1
|
||||
while (i <= length(s)) {
|
||||
c = substr(s, i, 1)
|
||||
width = 1
|
||||
if (c ~ ("^[\302-\337]$")) width = 2
|
||||
else if (c ~ ("^[\340-\357]$")) width = 3
|
||||
else if (c ~ ("^[\360-\367]$")) width = 4
|
||||
if (i + width - 1 > max_bytes) break
|
||||
out = out substr(s, i, width)
|
||||
i += width
|
||||
}
|
||||
return out
|
||||
}
|
||||
function summary_kv(s, key, part) {
|
||||
s = normalize(s)
|
||||
if (!match(s, "(^|[[:space:]])" key "=[^[:space:]]+")) return ""
|
||||
part = substr(s, RSTART, RLENGTH)
|
||||
sub(/^[^=]*=/, "", part)
|
||||
return part
|
||||
}
|
||||
function summary_rule(s, first) {
|
||||
s = trim(normalize(s))
|
||||
sub(/^\[[[:space:]]*[0-9.]+\][[:space:]]*/, "", s)
|
||||
if (tolower(s) ~ /^fw4:[[:space:]]*/) return "fw4"
|
||||
first = s
|
||||
sub(/[[:space:]:].*/, "", first)
|
||||
if (first == "" || first ~ /^(IN|OUT|SRC|DST|PROTO|SPT|DPT|LEN|MAC|TYPE|CODE|TTL|TOS|PREC|DF)=/) return ""
|
||||
if (tolower(first) == "kernel" || tolower(first) == "iptables") return ""
|
||||
return utf8_prefix(first, 64)
|
||||
}
|
||||
function summary_add_count(counts, key) {
|
||||
if (key != "") counts[key]++
|
||||
}
|
||||
function json_escape(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 == "\n") out = out "\\n"
|
||||
else if (c == "\r") out = out "\\r"
|
||||
else if (c == "\t") out = out "\\t"
|
||||
else if (c < " ") out = out " "
|
||||
else out = out c
|
||||
}
|
||||
return out
|
||||
}
|
||||
function summary_top(counts, limit, out, i, k, best, best_n) {
|
||||
for (k in summary_picked) delete summary_picked[k]
|
||||
out = "["
|
||||
for (i = 0; i < limit; i++) {
|
||||
best = ""; best_n = 0
|
||||
for (k in counts) {
|
||||
if (!(k in summary_picked) && (counts[k] > best_n || (counts[k] == best_n && (best == "" || k < best)))) {
|
||||
best = k; best_n = counts[k]
|
||||
}
|
||||
}
|
||||
if (best == "") break
|
||||
summary_picked[best] = 1
|
||||
if (i) out = out ","
|
||||
out = out "{\"value\":\"" json_escape(best) "\",\"count\":" best_n "}"
|
||||
}
|
||||
return out "]"
|
||||
}
|
||||
function summary_add(s, action, src, dst, rule) {
|
||||
summary_count++
|
||||
action = tolower(detect_action(normalize(s)))
|
||||
if (action == "drop" || action == "reject" || action == "deny" || action == "block") summary_add_count(summary_drop_counts, action)
|
||||
src = summary_kv(s, "SRC")
|
||||
if (src == "") src = summary_kv(s, "DST")
|
||||
if (src != "") summary_add_count(summary_talker_counts, utf8_prefix(src, 64))
|
||||
rule = summary_rule(s)
|
||||
if (rule != "") summary_add_count(summary_rule_counts, rule)
|
||||
}
|
||||
function summary_json(out) {
|
||||
out = "{\"scope\":\"top of shown sample\",\"top_talkers\":" summary_top(summary_talker_counts, 3)
|
||||
out = out ",\"top_drops\":" summary_top(summary_drop_counts, 3)
|
||||
out = out ",\"top_rules\":" summary_top(summary_rule_counts, 3) "}"
|
||||
# LC_ALL=C makes this a byte budget; utf8_prefix keeps field cuts valid.
|
||||
if (length(out) > 256) return "{\"scope\":\"top of shown sample\",\"truncated\":true}"
|
||||
return out
|
||||
}
|
||||
BEGIN {
|
||||
if (MODE == "json_reply") {
|
||||
ORS = ""
|
||||
printf "{\"log\":["
|
||||
} else if (MODE != "json") ORS = ""
|
||||
}
|
||||
{
|
||||
if (MODE == "json") {
|
||||
if (MODE == "json" || MODE == "json_reply") {
|
||||
msg = json_get_msg($0)
|
||||
if (is_fw(msg)) {
|
||||
if (MODE == "json_reply" && SUMMARY != "0") summary_add(msg)
|
||||
if (out_n++) printf ","
|
||||
printf "%s", $0
|
||||
}
|
||||
@@ -119,6 +209,11 @@ BEGIN { if (MODE != "json") ORS = "" }
|
||||
buf = (NR == 1) ? $0 : buf "\n" $0
|
||||
}
|
||||
END {
|
||||
if (MODE != "json")
|
||||
if (MODE == "json_reply") {
|
||||
printf "],\"messages_received\":%d", NR
|
||||
if (SUMMARY != "0" && summary_count > 0) printf ",\"summary\":%s", summary_json()
|
||||
printf "}"
|
||||
}
|
||||
else if (MODE != "json")
|
||||
print is_fw(buf) ? 1 : 0
|
||||
}
|
||||
@@ -5,14 +5,18 @@
|
||||
# source: core/fwlive-log.js CLASSIFY_SPEC
|
||||
# Shared isFirewallEvent parity logic (shell). Sourced by fwlive-log-filter.sh and tests.
|
||||
# Sourced library: do not add set -euo here (callers own strict mode, #291 C3).
|
||||
# One awk process classifies a batch (MODE=json) or one message (default).
|
||||
# One awk process classifies a batch (MODE=json/json_reply) or one message (default).
|
||||
|
||||
# The caller sets FILTER_DIR when this file is sourced. Resolve the asset
|
||||
# once, not once per classification call.
|
||||
CLASSIFY_AWK="${FILTER_DIR:-${0%/*}}/fwlive-is-firewall-event.awk"
|
||||
|
||||
_fwlive_run_classify() {
|
||||
awk -v MODE="${1:-msg}" -f "$CLASSIFY_AWK"
|
||||
if [ "${1:-msg}" = json_reply ]; then
|
||||
LC_ALL=C awk -v MODE="${1:-msg}" -v SUMMARY="${FWLIVE_SUMMARY:-1}" -f "$CLASSIFY_AWK"
|
||||
else
|
||||
awk -v MODE="${1:-msg}" -v SUMMARY="${FWLIVE_SUMMARY:-1}" -f "$CLASSIFY_AWK"
|
||||
fi
|
||||
}
|
||||
|
||||
is_firewall_event_msg() {
|
||||
@@ -23,3 +27,7 @@ is_firewall_event_msg() {
|
||||
_fwlive_filter_json_entries() {
|
||||
_fwlive_run_classify json
|
||||
}
|
||||
|
||||
_fwlive_filter_json_reply() {
|
||||
_fwlive_run_classify json_reply
|
||||
}
|
||||
@@ -6,14 +6,13 @@
|
||||
# Log messages are treated as data (jsonfilter + awk stdin); never interpolated
|
||||
# into shell command strings. Usage: ubus call log read '...' | fwlive-log-filter.sh
|
||||
#
|
||||
# Perf: one jsonfilter for @.log[*] plus one awk classify. Process
|
||||
# count is constant per poll, not O(entries).
|
||||
# Perf (#219): one jsonfilter for @.log[*] plus one awk classify. The same
|
||||
# awk pass counts enumerated entries for messages_received; process count is
|
||||
# constant per poll, not O(entries).
|
||||
#
|
||||
# Entry point (pipeline). The classifier sibling is sourced and must not set
|
||||
# strict mode itself.
|
||||
# strict mode itself (#291 C3).
|
||||
set -eu
|
||||
# shellcheck disable=SC3040
|
||||
(set -o pipefail) 2>/dev/null && set -o pipefail
|
||||
|
||||
if ! command -v jsonfilter >/dev/null 2>&1; then
|
||||
command -v logger >/dev/null 2>&1 && logger -t fwlive "jsonfilter not found; cannot filter firewall logs"
|
||||
@@ -26,17 +25,35 @@ FILTER_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
. "$FILTER_DIR/fwlive-is-firewall-event.sh"
|
||||
|
||||
# The classifier is a package asset, not generated at request time. Fail
|
||||
# closed if an incomplete install or a damaged package removed it.
|
||||
# closed if an incomplete install or a damaged package removed it (#321).
|
||||
if [ ! -r "$CLASSIFY_AWK" ]; then
|
||||
printf '%s' '{"log":[],"error":"classifier_missing"}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s' '{"log":['
|
||||
# Prefer stdin over -s: Linux MAX_ARG_STRLEN is 128KiB; a raised logd ring
|
||||
# (or paused FETCH_LINES_MAX poll) can exceed that and make jsonfilter fail
|
||||
# while this script still printed {"log":[]} and exited 0.
|
||||
# jsonfilter miss / empty @.log is not fatal; must still close JSON.
|
||||
# set -e + pipefail cannot apply to this pipeline.
|
||||
jsonfilter -e '@.log[*]' 2>/dev/null | _fwlive_filter_json_entries || true
|
||||
printf '%s' ']}'
|
||||
# while this script still returns a valid empty result (#234).
|
||||
# A valid input with no log entries is not fatal; the classifier closes JSON
|
||||
# and reports messages_received:0 (#220). A jsonfilter failure is different:
|
||||
# it must remain an error so rpcd does not record a fast healthy sample. Keep
|
||||
# jsonfilter output in a secure temporary file before classification so a
|
||||
# partial pipeline cannot produce malformed JSON on failure.
|
||||
# This script runs from rpcd as root and reopens the path for classification.
|
||||
# Match _fwlive_mktemp: only use the verified, sticky system /tmp; honoring
|
||||
# TMPDIR would permit a non-sticky attacker-controlled directory here.
|
||||
# shellcheck disable=SC3065 # OpenWrt BusyBox test supports -k; match rpcd helper.
|
||||
if [ ! -d /tmp ] || [ -L /tmp ] || ! [ -k /tmp ]; then
|
||||
printf '%s' '{"log":[],"error":"filter_tempfile_failed"}'
|
||||
exit 1
|
||||
fi
|
||||
_filter_tmp=$(mktemp /tmp/fwlive-filter.XXXXXX) || {
|
||||
printf '%s' '{"log":[],"error":"filter_tempfile_failed"}'
|
||||
exit 1
|
||||
}
|
||||
trap 'rm -f "$_filter_tmp"' 0 1 2 3 15
|
||||
if ! jsonfilter -e '@.log[*]' >"$_filter_tmp" 2>/dev/null; then
|
||||
printf '%s' '{"log":[],"error":"filter_failed"}'
|
||||
exit 1
|
||||
fi
|
||||
_fwlive_filter_json_reply <"$_filter_tmp"
|
||||
@@ -27,6 +27,54 @@ NF_LOG_IPV6='/proc/sys/net/netfilter/nf_log/10'
|
||||
WAN_LOG_LOCK_FILE="${FWLIVE_WAN_LOG_LOCK_FILE:-/etc/fwlive/logging.lock}"
|
||||
WAN_LOG_BASELINE_FILE="${FWLIVE_WAN_LOG_BASELINE_FILE:-/etc/fwlive/wan-log-baseline}"
|
||||
|
||||
weak_device_detected() {
|
||||
# Path overrides are test hooks; production defaults stay in procfs.
|
||||
_meminfo=${FWLIVE_MEMINFO_PATH:-/proc/meminfo}
|
||||
_cpuinfo=${FWLIVE_CPUINFO_PATH:-/proc/cpuinfo}
|
||||
_mem_total=
|
||||
if [ -r "$_meminfo" ]; then
|
||||
while IFS= read -r _line; do
|
||||
case "$_line" in
|
||||
MemTotal:*)
|
||||
_mem_total=${_line#MemTotal:}
|
||||
while :; do
|
||||
case "$_mem_total" in
|
||||
[[:space:]]*) _mem_total=${_mem_total#?} ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
_mem_total=${_mem_total%%[[:space:]]*}
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done <"$_meminfo" || :
|
||||
fi
|
||||
|
||||
_cores=0
|
||||
_cpu_valid=0
|
||||
if [ -r "$_cpuinfo" ]; then
|
||||
while IFS= read -r _line; do
|
||||
case "$_line" in
|
||||
processor[[:space:]]*:*)
|
||||
_cores=$((_cores + 1))
|
||||
_cpu_valid=1
|
||||
;;
|
||||
esac
|
||||
done <"$_cpuinfo" || :
|
||||
fi
|
||||
|
||||
case "$_mem_total" in
|
||||
''|*[!0-9]*) printf 'false\n'; return 0 ;;
|
||||
esac
|
||||
[ "$_cpu_valid" = 1 ] || { printf 'false\n'; return 0; }
|
||||
# MemTotal is reported in KiB; 256 MiB is 262144 KiB.
|
||||
if [ "$_mem_total" -lt 262144 ] || [ "$_cores" -le 1 ]; then
|
||||
printf 'true\n'
|
||||
else
|
||||
printf 'false\n'
|
||||
fi
|
||||
}
|
||||
|
||||
# RFC 8259 string escape. Lives here so prerm can source this file
|
||||
# standalone. rpcd sources us and must not redefine this.
|
||||
json_escape() {
|
||||
@@ -470,12 +518,14 @@ build_logging_status_json() {
|
||||
if [ -n "$zone" ] && [ "$wan_log" = true ] && [ "$nf4" = true ] && [ "$nf6" = true ]; then
|
||||
ready=true
|
||||
fi
|
||||
weak_device=false
|
||||
[ "$(weak_device_detected)" = true ] && weak_device=true
|
||||
|
||||
zone_json=$(json_null_or_string "$zone")
|
||||
limit_json=$(json_null_or_string "$limit_val")
|
||||
|
||||
printf '{"wan_zone":%s,"wan_log":%s,"wan_log_limit":%s,"nf_log_ipv4":%s,"nf_log_ipv6":%s,"ready":%s,"blockers":%s,"warnings":%s}' \
|
||||
"$zone_json" "$wan_log" "$limit_json" "$nf4" "$nf6" "$ready" "$blockers" "$warnings"
|
||||
printf '{"wan_zone":%s,"wan_log":%s,"wan_log_limit":%s,"nf_log_ipv4":%s,"nf_log_ipv6":%s,"ready":%s,"weak_device":%s,"blockers":%s,"warnings":%s}' \
|
||||
"$zone_json" "$wan_log" "$limit_json" "$nf4" "$nf6" "$ready" "$weak_device" "$blockers" "$warnings"
|
||||
}
|
||||
|
||||
reload_firewall() {
|
||||
|
||||
@@ -23,7 +23,7 @@ ADAPTIVE_SH="$LIBEXEC_DIR/fwlive-adaptive-cap.sh"
|
||||
RESOLVE_MAX=32
|
||||
# Per-lookup nslookup timeout (integer seconds — BusyBox `timeout` truncates
|
||||
# fractional values to 0 on OpenWrt 23.05/1.36, so 0.5 would silently
|
||||
# degrade to no timeout.. The wall-clock budget
|
||||
# degrade to no timeout; luna fold 2026-08-10). The wall-clock budget
|
||||
# below is the real whole-call bound; this per-lookup cap only prevents a
|
||||
# single hung resolver from dominating the budget. LAN/local-resolver
|
||||
# lookups are single-digit ms, so 1s is ample. Passed as argv to
|
||||
@@ -42,7 +42,7 @@ NFT_TIMEOUT=5
|
||||
# CLI-only iptables fixture path for __rulesmap_iptables (no arbitrary paths).
|
||||
RULESMAP_IPTABLES_FILE='/tmp/rulesmap'
|
||||
|
||||
# json_escape lives in fwlive-logging.sh so prerm can source that file alone.
|
||||
# json_escape lives in fwlive-logging.sh so prerm can source that file alone (#222).
|
||||
# shellcheck disable=SC1090,SC1091 # LOGGING_SH resolves to sibling ../fwlive-logging.sh
|
||||
. "$LOGGING_SH"
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
@@ -52,10 +52,10 @@ slug_key() {
|
||||
echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' _' '--'
|
||||
}
|
||||
|
||||
# Cap rules JSON size so a huge ruleset cannot unbound an rpcd reply.
|
||||
# Cap rules JSON size so a huge ruleset cannot unbound an rpcd reply (#229).
|
||||
RULES_MAP_MAX_KEYS=512
|
||||
RULES_MAP_MAX_BYTES=65536
|
||||
# Same wall-clock bound as nft for iptables-save / ip6tables-save.
|
||||
# Same wall-clock bound as nft for iptables-save / ip6tables-save (#229).
|
||||
# Overridable for host tests (e.g. FWLIVE_IPTABLES_TIMEOUT=1).
|
||||
IPTABLES_TIMEOUT="${FWLIVE_IPTABLES_TIMEOUT:-5}"
|
||||
|
||||
@@ -70,7 +70,7 @@ map_add() {
|
||||
# ("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.
|
||||
# cosmetics so first-wins prefers authoritative names (#230).
|
||||
case "$OUT" in
|
||||
*"\"$esc_key\":"*) return 0 ;;
|
||||
esac
|
||||
@@ -92,7 +92,7 @@ map_add() {
|
||||
|
||||
uci_rule_names() {
|
||||
# uci missing / empty firewall config is "no names", not fatal.
|
||||
# pipefail + set -e cannot apply to this pipeline.
|
||||
# pipefail + set -e cannot apply to this pipeline (#291 C3).
|
||||
uci -q show firewall 2>/dev/null | sed -n "s/^firewall\.@rule\[[0-9]*\]\.name='\(.*\)'$/\1/p" || true
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ run_with_timeout() {
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout "$secs" "$@" 2>/dev/null
|
||||
else
|
||||
# Fail closed: never run unbounded when timeout is absent.
|
||||
# Fail closed (#229): never run unbounded when timeout is absent.
|
||||
return 127
|
||||
fi
|
||||
}
|
||||
@@ -170,7 +170,7 @@ map_uci_rule_names() {
|
||||
# Line-wise (here-doc, not an unquoted for-loop) so a name with
|
||||
# spaces stays one token. Names that fail is_uci_style_name are
|
||||
# skipped: parseRuleHint() cannot match them, and fragments of
|
||||
# "My Rule" must not shadow a real rule named Rule.
|
||||
# "My Rule" must not shadow a real rule named Rule (#226).
|
||||
# The while-read stays in the current shell (here-doc, not a
|
||||
# pipeline) so map_add mutations reach OUT.
|
||||
while IFS= read -r name || [ -n "$name" ]; do
|
||||
@@ -187,7 +187,7 @@ EOF
|
||||
}
|
||||
|
||||
# pass=labeled|unlabeled|all — labeled before unlabeled so !fw4: beats
|
||||
# an earlier cosmetic for the same prefix under first-wins.
|
||||
# an earlier cosmetic for the same prefix under first-wins (#230).
|
||||
map_from_nft_stream() {
|
||||
pass="${1:-all}"
|
||||
while IFS= read -r line; do
|
||||
@@ -288,7 +288,7 @@ build_rules_map() {
|
||||
backend=$(detect_firewall_backend)
|
||||
|
||||
# UCI first, then labeled stream prefixes, then unlabeled cosmetics
|
||||
# First-wins keeps UCI over nft and !fw4: over cosmetic.
|
||||
# (#230). First-wins keeps UCI over nft and !fw4: over cosmetic.
|
||||
map_uci_rule_names
|
||||
|
||||
case "$backend" in
|
||||
@@ -435,32 +435,54 @@ fetch_firewall_logs() {
|
||||
shed_flag=${2:-0}
|
||||
# Adaptive plan prints: lines shed bucket
|
||||
start_cs=$(fwlive_adaptive_clock_cs)
|
||||
# Drop dead oneshot flag — only consumed on logd's stream branch.
|
||||
# Drop dead oneshot flag — only consumed on logd's stream branch (#306).
|
||||
if ! raw=$(ubus call log read "{\"lines\":$lines,\"stream\":false}" 2>/dev/null); then
|
||||
# Do not record ~0 ms as cold: failure is not healthy fast processing
|
||||
# and must not clear an existing hot/shed cap.
|
||||
# and must not clear an existing hot/shed cap (#329 Hermes Q1).
|
||||
out='{"log":[],"error":"log_read_failed"}'
|
||||
fwlive_adaptive_merge_reply "$out" "$shed_flag" "$lines" 0 0
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Do not swallow a filter failure into a silent empty table.
|
||||
# Summary aggregation is part of the adaptive contract. Keep it out of
|
||||
# adaptive=0 replies, while the classifier still remains the single pass
|
||||
# over decoded log entries (#306).
|
||||
if fwlive_adaptive_enabled; then
|
||||
FWLIVE_SUMMARY=1
|
||||
else
|
||||
FWLIVE_SUMMARY=0
|
||||
fi
|
||||
export FWLIVE_SUMMARY
|
||||
|
||||
# Do not swallow a filter failure into a silent empty table (#220).
|
||||
# If the filter exits non-zero but still printed JSON, keep it.
|
||||
# Empty success is not valid poll JSON — the shipped filter always
|
||||
# prints {"log":[...]}, but a stub/regression must not emit a blank body.
|
||||
_filter_ok=1
|
||||
if ! out=$(printf '%s' "$raw" | "$FILTER_SH"); then
|
||||
_filter_ok=0
|
||||
if [ -z "$out" ]; then
|
||||
out='{"log":[],"error":"filter_failed"}'
|
||||
fi
|
||||
fi
|
||||
[ -n "$out" ] || out='{"log":[],"error":"filter_empty"}'
|
||||
if [ -z "$out" ]; then
|
||||
_filter_ok=0
|
||||
out='{"log":[],"error":"filter_empty"}'
|
||||
fi
|
||||
# A classifier can return a structured error body while exiting zero (for
|
||||
# example classifier_missing). Preserve the error for the caller, but do
|
||||
# not let it overwrite a prior hot state as a healthy fast sample.
|
||||
case "$out" in
|
||||
*'"error":'*) _filter_ok=0 ;;
|
||||
esac
|
||||
end_cs=$(fwlive_adaptive_clock_cs)
|
||||
# Uptime wrap / skew: end < start — do not record as cold.
|
||||
if [ "$end_cs" -ge "$start_cs" ]; then
|
||||
# Uptime wrap / skew: end < start — do not record as cold (Grok #329 P2).
|
||||
if [ "$_filter_ok" = 1 ] && [ "$end_cs" -ge "$start_cs" ]; then
|
||||
_dur=$(((end_cs - start_cs) * 10))
|
||||
fwlive_adaptive_record "$_dur" "$lines"
|
||||
fi
|
||||
# Layer 1: messages_received stays 0 — no post-filter ash count.
|
||||
# The filter carries messages_received from its jsonfilter enumeration;
|
||||
# failed/read-invalid paths keep the merge fallback at 0.
|
||||
_trunc=0
|
||||
[ "$shed_flag" = 1 ] && _trunc=1
|
||||
[ "$lines" -lt "$requested" ] && _trunc=1
|
||||
@@ -502,7 +524,7 @@ resolve_hostname() {
|
||||
[ -n "$ip" ] || return 1
|
||||
is_resolvable_address "$ip" || return 1
|
||||
command -v nslookup >/dev/null 2>&1 || return 1
|
||||
# timeout/nslookup miss is "no PTR", not a plugin abort.
|
||||
# timeout/nslookup miss is "no PTR", not a plugin abort (#291 C3).
|
||||
out=$(run_with_timeout "$RESOLVE_TIMEOUT" nslookup "$ip") || return 1
|
||||
[ -n "$out" ] || return 1
|
||||
name=$(parse_nslookup_name "$out") || return 1
|
||||
@@ -510,7 +532,7 @@ resolve_hostname() {
|
||||
printf '%s' "$name"
|
||||
}
|
||||
|
||||
# Strict IPv4/IPv6 validation before nslookup: the old char-class
|
||||
# Strict IPv4/IPv6 validation before nslookup (issue #190): the old char-class
|
||||
# check admitted hostname-shaped tokens of hex chars + dots/colons
|
||||
# (e.g. "dead.beef.cafe.baad"), letting the resolver fall back to upstream DNS
|
||||
# queries chosen by an authenticated session. Validate the real address
|
||||
@@ -523,7 +545,7 @@ is_resolvable_address() {
|
||||
# Reject anything outside the address alphabet BEFORE awk, so embedded
|
||||
# newlines/whitespace (which awk would treat as record separators) can
|
||||
# never let a valid first record mask trailing garbage that still reaches
|
||||
# nslookup: nslookup only ever sees a single
|
||||
# nslookup (#190, CodeRabbit/luna fold): nslookup only ever sees a single
|
||||
# token. Note the check intentionally allows only hex/colon/dot.
|
||||
case "$addr" in
|
||||
*[!0-9a-fA-F:.]*) return 1 ;;
|
||||
@@ -596,7 +618,7 @@ is_resolvable_address() {
|
||||
}
|
||||
|
||||
resolve_addresses() {
|
||||
# Missing resolver must not look like "no PTR". Check before
|
||||
# Missing resolver must not look like "no PTR" (#218). Check before
|
||||
# starting the budget clock or reading stdin.
|
||||
if ! command -v nslookup >/dev/null 2>&1; then
|
||||
command -v logger >/dev/null 2>&1 && logger -t fwlive "nslookup not found; resolve disabled"
|
||||
@@ -604,14 +626,14 @@ resolve_addresses() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Load shed: previous poll was hot.
|
||||
# Load shed: previous poll was hot (#306 Layer 1).
|
||||
if fwlive_adaptive_is_hot; then
|
||||
printf '{"names":{},"disabled":"load"}'
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Wall-clock budget clock starts at the TRUE function entry: the
|
||||
# budget must bound the whole call — including
|
||||
# Wall-clock budget clock starts at the TRUE function entry (luna
|
||||
# folds 2026-08-10): the budget must bound the whole call — including
|
||||
# read_rpc_input (stalled stdin must not escape the budget) and json
|
||||
# parsing. `date +%s` can jump under NTP sync — accepted (worst case
|
||||
# the budget over- or under-runs by the jump; worker starvation is
|
||||
@@ -657,12 +679,12 @@ resolve_addresses() {
|
||||
# never START a lookup past the budget.
|
||||
now=$(date +%s)
|
||||
[ $((now - start)) -ge "$RESOLVE_BUDGET" ] && break
|
||||
# Missing/empty element: set -u cannot read bare $ip.
|
||||
# Missing/empty element: set -u cannot read bare $ip (#291 C3).
|
||||
if ! is_resolvable_address "${ip:-}"; then
|
||||
continue
|
||||
fi
|
||||
# NXDOMAIN / timeout: resolve_hostname returns 1. set -e
|
||||
# cannot apply to this assignment.
|
||||
# cannot apply to this assignment (#291 C3).
|
||||
name=$(resolve_hostname "$ip") || name=
|
||||
if [ -n "$name" ]; then
|
||||
map_add "$ip" "$name"
|
||||
@@ -729,7 +751,7 @@ run_selftest() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# hostname-shaped tokens of hex chars + dots/colons passed the
|
||||
# Issue #190: hostname-shaped tokens of hex chars + dots/colons passed the
|
||||
# old char-class check and made the resolver fall back to upstream DNS. Each case
|
||||
# below must REJECT (and would be ACCEPTED by the reverted char-class code).
|
||||
if is_resolvable_address 'ab.cd.ef.01'; then
|
||||
@@ -757,7 +779,7 @@ run_selftest() {
|
||||
echo 'is_resolvable_address: expected accept IPv6-mapped ::ffff:192.0.2.1' >&2
|
||||
return 1
|
||||
fi
|
||||
# Multi-line input must be rejected wholesale: awk
|
||||
# Multi-line input must be rejected wholesale (CodeRabbit/luna fold): awk
|
||||
# reads newline-separated records, so a valid first record must not mask
|
||||
# trailing garbage that would still reach nslookup.
|
||||
if is_resolvable_address "$(printf '192.0.2.1\nnot-an-address')"; then
|
||||
@@ -849,7 +871,7 @@ Address: 127.0.0.1:53")
|
||||
|
||||
run_logging_selftest || return 1
|
||||
|
||||
# Strict-mode smoke: status JSON uses soft-fail helpers
|
||||
# Strict-mode smoke (#291 C3): status JSON uses soft-fail helpers
|
||||
# (uci miss, empty WAN zone). Must not abort under dash/ash set -e.
|
||||
_status=$(build_logging_status_json) || {
|
||||
echo "build_logging_status_json: aborted under set -e" >&2
|
||||
@@ -950,7 +972,7 @@ case "${1:-}" in
|
||||
build_rules_map
|
||||
;;
|
||||
poll)
|
||||
# rpcd may omit argv $3 and pass JSON on stdin.
|
||||
# rpcd may omit argv $3 and pass JSON on stdin (#291 C3).
|
||||
poll_logs "${3:-}"
|
||||
;;
|
||||
resolve)
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.9.12
|
||||
PKG_RELEASE:=123
|
||||
PKG_RELEASE:=124
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -2174,23 +2174,8 @@ msgstr "حذف قوانین %s کامل شد."
|
||||
msgid "%s firewall rules load complete!"
|
||||
msgstr "بارگذاری قوانین فایروال %s کامل شد!"
|
||||
|
||||
msgid "Socks switch detection: Unknown error."
|
||||
msgstr "تشخیص سوئیچ Socks: خطای ناشناخته."
|
||||
|
||||
msgid "Socks switch detection: Unable to connect to the network. Please check if the network is working properly!"
|
||||
msgstr "تشخیص سوئیچ Socks: عدم توانایی در اتصال به شبکه. لطفاً بررسی کنید که شبکه به درستی کار میکند!"
|
||||
|
||||
msgid "Socks switch detection: Primary node 【%s: [%s]】 is normal. Switch to the primary node!"
|
||||
msgstr "تشخیص سوئیچ Socks: گره اولیه 【%s: [%s]】 عادی است. سوئیچ به گره اولیه!"
|
||||
|
||||
msgid "Socks switch detection: %s node switch complete!"
|
||||
msgstr "تشخیص سوئیچ Socks: سوئیچ گره %s کامل شد!"
|
||||
|
||||
msgid "Socks switch detection: %s 【%s:[%s]】 normal."
|
||||
msgstr "تشخیص سوئیچ Socks: %s 【%s:[%s]】 عادی است."
|
||||
|
||||
msgid "switch to %s test detect!"
|
||||
msgstr "سوئیچ به تست تشخیص %s!"
|
||||
msgid "main node"
|
||||
msgstr "گره اصلی"
|
||||
|
||||
msgid "backup node"
|
||||
msgstr "گره پشتیبان"
|
||||
@@ -2198,17 +2183,35 @@ msgstr "گره پشتیبان"
|
||||
msgid "next backup node"
|
||||
msgstr "گره پشتیبان بعدی"
|
||||
|
||||
msgid "main node"
|
||||
msgstr "گره اصلی"
|
||||
msgid "Socks switch detection: Port [%s] try %s [%s:%s]."
|
||||
msgstr "تشخیص تغییر وضعیت سوئیچ: پورت [%s]، تلاش %s [%s:%s]."
|
||||
|
||||
msgid "Socks switch detection: Unknown error."
|
||||
msgstr "تشخیص سوئیچ Socks: خطای ناشناخته."
|
||||
msgid "Socks switch detection: Port [%s] [%s:[%s]] normal, switch to this node!"
|
||||
msgstr "تشخیص تغییر وضعیت سوکتها: پورت [%s] [%s:[%s]] در وضعیت عادی است، تغییر وضعیت به این گره!"
|
||||
|
||||
msgid "Socks switch detection: %s 【%s:[%s]】 abnormal, %s"
|
||||
msgstr "تشخیص سوئیچ Socks: %s 【%s:[%s]】 غیرعادی، %s"
|
||||
msgid "Socks switch detection: Port [%s] node switch complete!"
|
||||
msgstr "تشخیص تغییر وضعیت سوکتها: تغییر وضعیت گره در پورت [%s] تکمیل شد!"
|
||||
|
||||
msgid "Socks switch detection: %s 【%s:[%s]】 normal, switch to this node!"
|
||||
msgstr "تشخیص سوئیچ Socks: %s 【%s:[%s]】 عادی، سوئیچ به این گره!"
|
||||
msgid "Socks switch detection: Port [%s] [%s:[%s]] abnormal."
|
||||
msgstr "تشخیص وضعیت سوئیچ: پورت [%s] [%s:[%s]] غیرعادی است."
|
||||
|
||||
msgid "Socks switch detection: Port [%s] all nodes are unavailable!"
|
||||
msgstr "تشخیص تغییر وضعیت سوئیچ: در پورت [%s]، تمام گرهها در دسترس نیستند!"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Unknown error."
|
||||
msgstr "تشخیص وضعیت سوئیچ جوراب: پورت [%s] خطای ناشناخته."
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Unable to connect to the network. Please check if the network is working properly!"
|
||||
msgstr "تشخیص تغییر وضعیت سوکت: پورت [%s]؛ امکان اتصال به شبکه وجود ندارد. لطفاً بررسی کنید که آیا شبکه بهدرستی کار میکند یا خیر."
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Primary node [%s: [%s]] is normal. Switch to the primary node!"
|
||||
msgstr "تشخیص تغییر وضعیت سوکت: وضعیت پورت [%s] و گره اصلی [%s: [%s]] عادی است. تغییر وضعیت به گره اصلی!"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] [%s:[%s]] normal."
|
||||
msgstr "تشخیص وضعیت سوئیچ: پورت [%s] [%s:[%s]] در وضعیت عادی است."
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Number of backup nodes: %s"
|
||||
msgstr "تشخیص سوئیچ سوئیچ شبکه: پورت [%s] تعداد گرههای پشتیبان: %s"
|
||||
|
||||
msgid "Restart dnsmasq service."
|
||||
msgstr "راهاندازی مجدد سرویس dnsmasq."
|
||||
|
||||
@@ -2175,23 +2175,8 @@ msgstr "Удаление правил %s завершено."
|
||||
msgid "%s firewall rules load complete!"
|
||||
msgstr "Загрузка правил фаервола %s завершена!"
|
||||
|
||||
msgid "Socks switch detection: Unknown error."
|
||||
msgstr "Проверка переключения SOCKS: неизвестная ошибка."
|
||||
|
||||
msgid "Socks switch detection: Unable to connect to the network. Please check if the network is working properly!"
|
||||
msgstr "Проверка переключения SOCKS: не удалось подключиться к сети. Пожалуйста, проверьте работоспособность сети!"
|
||||
|
||||
msgid "Socks switch detection: Primary node 【%s: [%s]】 is normal. Switch to the primary node!"
|
||||
msgstr "Проверка переключения SOCKS: основной сервер 【%s: [%s]】 работает нормально. Переключение на основной сервер!"
|
||||
|
||||
msgid "Socks switch detection: %s node switch complete!"
|
||||
msgstr "Проверка переключения SOCKS: переключение сервера %s завершено!"
|
||||
|
||||
msgid "Socks switch detection: %s 【%s:[%s]】 normal."
|
||||
msgstr "Проверка переключения SOCKS: %s 【%s:[%s]】 работает нормально."
|
||||
|
||||
msgid "switch to %s test detect!"
|
||||
msgstr "Переключение на %s для проверки!"
|
||||
msgid "main node"
|
||||
msgstr "основной сервер"
|
||||
|
||||
msgid "backup node"
|
||||
msgstr "резервный сервер"
|
||||
@@ -2199,14 +2184,35 @@ msgstr "резервный сервер"
|
||||
msgid "next backup node"
|
||||
msgstr "следующий резервный сервер"
|
||||
|
||||
msgid "main node"
|
||||
msgstr "основной сервер"
|
||||
msgid "Socks switch detection: Port [%s] try %s [%s:%s]."
|
||||
msgstr "Обнаружение переключения кабеля: порт [%s], попытка %s [%s:%s]."
|
||||
|
||||
msgid "Socks switch detection: %s 【%s:[%s]】 abnormal, %s"
|
||||
msgstr "Проверка переключения SOCKS: %s 【%s:[%s]】 не работает, %s"
|
||||
msgid "Socks switch detection: Port [%s] [%s:[%s]] normal, switch to this node!"
|
||||
msgstr "Обнаружение переключения Socks: порт [%s] [%s:[%s]] в норме, переключение на этот узел!"
|
||||
|
||||
msgid "Socks switch detection: %s 【%s:[%s]】 normal, switch to this node!"
|
||||
msgstr "Проверка переключения SOCKS: %s 【%s:[%s]】 работает нормально, переключение на этот сервер!"
|
||||
msgid "Socks switch detection: Port [%s] node switch complete!"
|
||||
msgstr "Обнаружение переключения соединений: переключение узла на порту [%s] завершено!"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] [%s:[%s]] abnormal."
|
||||
msgstr "Обнаружено переключение Socks: порт [%s] [%s:[%s]] работает некорректно."
|
||||
|
||||
msgid "Socks switch detection: Port [%s] all nodes are unavailable!"
|
||||
msgstr "Обнаружено переключение коммутатора: порт [%s], все узлы недоступны!"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Unknown error."
|
||||
msgstr "Обнаружение переключения разъема: порт [%s], неизвестная ошибка."
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Unable to connect to the network. Please check if the network is working properly!"
|
||||
msgstr "Обнаружено переключение Socks: порт [%s] — не удалось подключиться к сети. Пожалуйста, проверьте исправность сети!"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Primary node [%s: [%s]] is normal. Switch to the primary node!"
|
||||
msgstr "Обнаружено переключение: порт [%s], основной узел [%s: [%s]] — состояние нормальное. Переключение на основной узел!"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] [%s:[%s]] normal."
|
||||
msgstr "Обнаружение переключения Socks: порт [%s] [%s:[%s]] — норма."
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Number of backup nodes: %s"
|
||||
msgstr "Обнаружение переключения коммутатора: порт [%s], количество резервных узлов: %s"
|
||||
|
||||
msgid "Restart dnsmasq service."
|
||||
msgstr "Перезапуск службы dnsmasq."
|
||||
|
||||
@@ -2160,23 +2160,8 @@ msgstr "删除 %s 规则完成。"
|
||||
msgid "%s firewall rules load complete!"
|
||||
msgstr "%s 防火墙规则加载完成!"
|
||||
|
||||
msgid "Socks switch detection: Unknown error."
|
||||
msgstr "Socks切换检测:未知错误。"
|
||||
|
||||
msgid "Socks switch detection: Unable to connect to the network. Please check if the network is working properly!"
|
||||
msgstr "Socks切换检测:无法连接到网络,请检查网络是否正常!"
|
||||
|
||||
msgid "Socks switch detection: Primary node 【%s: [%s]】 is normal. Switch to the primary node!"
|
||||
msgstr "Socks切换检测:%s 主节点【%s:[%s]】正常,切换到主节点!"
|
||||
|
||||
msgid "Socks switch detection: %s node switch complete!"
|
||||
msgstr "Socks切换检测:%s 节点切换完毕!"
|
||||
|
||||
msgid "Socks switch detection: %s 【%s:[%s]】 normal."
|
||||
msgstr "Socks切换检测:%s 【%s:[%s]】 正常。"
|
||||
|
||||
msgid "switch to %s test detect!"
|
||||
msgstr "切换到 %s 检测!"
|
||||
msgid "main node"
|
||||
msgstr "主节点"
|
||||
|
||||
msgid "backup node"
|
||||
msgstr "备用节点"
|
||||
@@ -2184,17 +2169,35 @@ msgstr "备用节点"
|
||||
msgid "next backup node"
|
||||
msgstr "下一个备用节点"
|
||||
|
||||
msgid "main node"
|
||||
msgstr "主节点"
|
||||
msgid "Socks switch detection: Port [%s] try %s [%s:%s]."
|
||||
msgstr "Socks切换检测:端口 [%s] 尝试 %s 【%s:%s】。"
|
||||
|
||||
msgid "Socks switch detection: Unknown error."
|
||||
msgstr "Socks切换检测:未知错误。"
|
||||
msgid "Socks switch detection: Port [%s] [%s:[%s]] normal, switch to this node!"
|
||||
msgstr "Socks切换检测:端口 [%s] 【%s:[%s]】 正常,切换到此节点!"
|
||||
|
||||
msgid "Socks switch detection: %s 【%s:[%s]】 abnormal, %s"
|
||||
msgstr "Socks切换检测:%s 【%s:[%s]】 异常,%s"
|
||||
msgid "Socks switch detection: Port [%s] node switch complete!"
|
||||
msgstr "Socks切换检测:端口 [%s] 节点切换完毕!"
|
||||
|
||||
msgid "Socks switch detection: %s 【%s:[%s]】 normal, switch to this node!"
|
||||
msgstr "Socks切换检测:%s 【%s:[%s]】 正常,切换到此节点!"
|
||||
msgid "Socks switch detection: Port [%s] [%s:[%s]] abnormal."
|
||||
msgstr "Socks切换检测:端口 [%s] 异常。"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] all nodes are unavailable!"
|
||||
msgstr "Socks切换检测:端口 [%s] 所有节点均不可用!"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Unknown error."
|
||||
msgstr "Socks切换检测:端口 [%s] 未知错误。"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Unable to connect to the network. Please check if the network is working properly!"
|
||||
msgstr "Socks切换检测:端口 [%s] 无法连接到网络,请检查网络是否正常!"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Primary node [%s: [%s]] is normal. Switch to the primary node!"
|
||||
msgstr "Socks切换检测:端口 [%s] 主节点【%s:[%s]】正常,切换到主节点!"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] [%s:[%s]] normal."
|
||||
msgstr "Socks切换检测:端口 [%s] 【%s:[%s]】 正常。"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Number of backup nodes: %s"
|
||||
msgstr "Socks切换检测:端口 [%s] 后备节点数量:%s"
|
||||
|
||||
msgid "Restart dnsmasq service."
|
||||
msgstr "重启 dnsmasq 服务。"
|
||||
|
||||
@@ -2166,23 +2166,8 @@ msgstr "刪除 %s 規則完成。"
|
||||
msgid "%s firewall rules load complete!"
|
||||
msgstr "%s 防火墙規則加载完成!"
|
||||
|
||||
msgid "Socks switch detection: Unknown error."
|
||||
msgstr "Socks切換檢測:未知錯誤。"
|
||||
|
||||
msgid "Socks switch detection: Unable to connect to the network. Please check if the network is working properly!"
|
||||
msgstr "Socks切換檢測:無法連接到網絡,請檢查網絡是否正常!"
|
||||
|
||||
msgid "Socks switch detection: Primary node 【%s: [%s]】 is normal. Switch to the primary node!"
|
||||
msgstr "Socks切換檢測:%s 主節點【%s:[%s]】正常,切換到主節點!"
|
||||
|
||||
msgid "Socks switch detection: %s node switch complete!"
|
||||
msgstr "Socks切換檢測:%s 節點切換完毕!"
|
||||
|
||||
msgid "Socks switch detection: %s 【%s:[%s]】 normal."
|
||||
msgstr "Socks切換檢測:%s 【%s:[%s]】 正常。"
|
||||
|
||||
msgid "switch to %s test detect!"
|
||||
msgstr "切換到 %s 檢測!"
|
||||
msgid "main node"
|
||||
msgstr "主節點"
|
||||
|
||||
msgid "backup node"
|
||||
msgstr "備用節點"
|
||||
@@ -2190,17 +2175,35 @@ msgstr "備用節點"
|
||||
msgid "next backup node"
|
||||
msgstr "下一個備用節點"
|
||||
|
||||
msgid "main node"
|
||||
msgstr "主節點"
|
||||
msgid "Socks switch detection: Port [%s] try %s [%s:%s]."
|
||||
msgstr "Socks切換檢測:連接埠 [%s] 嘗試 %s 【%s:%s】。"
|
||||
|
||||
msgid "Socks switch detection: Unknown error."
|
||||
msgstr "Socks切換檢測:未知錯誤。"
|
||||
msgid "Socks switch detection: Port [%s] [%s:[%s]] normal, switch to this node!"
|
||||
msgstr "Socks切換偵測:連接埠 [%s] 【%s:[%s]】 正常,切換到此節點!"
|
||||
|
||||
msgid "Socks switch detection: %s 【%s:[%s]】 abnormal, %s"
|
||||
msgstr "Socks切換檢測:%s 【%s:[%s]】 异常,%s"
|
||||
msgid "Socks switch detection: Port [%s] node switch complete!"
|
||||
msgstr "Socks切換偵測:連接埠 [%s] 節點切換完畢!"
|
||||
|
||||
msgid "Socks switch detection: %s 【%s:[%s]】 normal, switch to this node!"
|
||||
msgstr "Socks切換檢測:%s 【%s:[%s]】 正常,切換到此節點!"
|
||||
msgid "Socks switch detection: Port [%s] [%s:[%s]] abnormal."
|
||||
msgstr "Socks切換偵測:連接埠 [%s] 異常。"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] all nodes are unavailable!"
|
||||
msgstr "Socks切換偵測:連接埠 [%s] 所有節點均不可用!"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Unknown error."
|
||||
msgstr "Socks切換偵測:連接埠 [%s] 未知錯誤。"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Unable to connect to the network. Please check if the network is working properly!"
|
||||
msgstr "Socks切換偵測:連接埠 [%s] 無法連接到網絡,請檢查網路是否正常!"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Primary node [%s: [%s]] is normal. Switch to the primary node!"
|
||||
msgstr "Socks切換偵測:連接埠 [%s] 主節點【%s:[%s]】正常,切換到主節點!"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] [%s:[%s]] normal."
|
||||
msgstr "Socks切換檢測:連接埠 [%s] 【%s:[%s]】 正常。"
|
||||
|
||||
msgid "Socks switch detection: Port [%s] Number of backup nodes: %s"
|
||||
msgstr "Socks切換偵測:連接埠 [%s] 後備節點數量:%s"
|
||||
|
||||
msgid "Restart dnsmasq service."
|
||||
msgstr "重啟 dnsmasq 服務。"
|
||||
|
||||
@@ -331,15 +331,10 @@ load_acl() {
|
||||
[ -n "$dns_redirect_port" ] && {
|
||||
#nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp udp dport 53 counter accept"
|
||||
#nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol tcp tcp dport 53 counter accept"
|
||||
if [ "$PROXY_IPV6" == "1" ]; then
|
||||
#nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp udp dport 53 counter accept"
|
||||
#nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp tcp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME nat_output oif lo meta l4proto udp udp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
nft "add rule $NFTABLE_NAME nat_output oif lo meta l4proto tcp tcp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
else
|
||||
nft "add rule $NFTABLE_NAME nat_output oif lo ip protocol udp udp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
nft "add rule $NFTABLE_NAME nat_output oif lo ip protocol tcp tcp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
fi
|
||||
#nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp udp dport 53 counter accept"
|
||||
#nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp tcp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME nat_output oif lo meta l4proto udp udp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
nft "add rule $NFTABLE_NAME nat_output oif lo meta l4proto tcp tcp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
log 2 "${msg}$(i18n "DNS will redirected to the dedicated DNS server [%s]." "${dns_redirect_port}")"
|
||||
}
|
||||
fi
|
||||
@@ -504,26 +499,16 @@ load_acl() {
|
||||
}
|
||||
|
||||
if ([ -z "$no_tcp_proxy" ] || [ -z "$no_udp_proxy" ]) && [ -n "$dns_redirect_port" ]; then
|
||||
if [ "$PROXY_IPV6" == "1" ]; then
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} udp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} tcp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
else
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} udp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp ${_ipt_source} tcp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS ip protocol udp ${_ipt_source} udp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS ip protocol tcp ${_ipt_source} tcp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
fi
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} udp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp ${_ipt_source} tcp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} udp dport 53 counter accept" 2>/dev/null
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} tcp dport 53 counter accept" 2>/dev/null
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
log 2 "${msg}$(i18n "DNS will redirected to the dedicated DNS server [%s]." "${dns_redirect_port}")"
|
||||
else
|
||||
if [ "$PROXY_IPV6" == "1" ]; then
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter return comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter return comment \"$remarks\""
|
||||
else
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS ip protocol udp ${_ipt_source} udp dport 53 counter return comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS ip protocol tcp ${_ipt_source} tcp dport 53 counter return comment \"$remarks\""
|
||||
fi
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter return comment \"$remarks\"" 2>/dev/null
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter return comment \"$remarks\"" 2>/dev/null
|
||||
fi
|
||||
|
||||
[ -z "$no_tcp_proxy" ] && [ -n "$redir_port" ] && {
|
||||
@@ -828,6 +813,7 @@ add_firewall_rule() {
|
||||
# jump chains
|
||||
# Only TCP, UDP Invalid.
|
||||
nft "add rule $NFTABLE_NAME mangle_prerouting meta nfproto ipv4 meta l4proto tcp socket transparent 1 mark set ${FWMARK} counter accept comment PSW2_SOCKET"
|
||||
|
||||
nft "add rule $NFTABLE_NAME mangle_prerouting ip daddr != @$NFTSET_DIRECT ip protocol udp counter jump PSW2_MANGLE"
|
||||
[ -n "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME mangle_prerouting ip daddr != @$NFTSET_DIRECT ip protocol tcp counter jump PSW2_MANGLE"
|
||||
|
||||
@@ -899,9 +885,9 @@ add_firewall_rule() {
|
||||
[ "$PROXY_IPV6" == "1" ] && {
|
||||
# Only TCP, UDP Invalid.
|
||||
nft "add rule $NFTABLE_NAME mangle_prerouting meta nfproto ipv6 meta l4proto tcp socket transparent 1 mark set ${FWMARK} counter accept comment PSW2_SOCKET"
|
||||
|
||||
nft "add rule $NFTABLE_NAME mangle_prerouting ip6 daddr != @$NFTSET_DIRECT6 meta nfproto {ipv6} counter jump PSW2_MANGLE_V6"
|
||||
nft "add rule $NFTABLE_NAME mangle_output ip6 daddr != @$NFTSET_DIRECT6 meta nfproto {ipv6} counter jump PSW2_OUTPUT_MANGLE_V6 comment \"PSW2_OUTPUT_MANGLE\""
|
||||
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
|
||||
|
||||
ip -6 rule add fwmark ${FWMARK} table 999 priority 999
|
||||
|
||||
@@ -12,13 +12,11 @@ check_process() {
|
||||
}
|
||||
|
||||
test_url() {
|
||||
local url=$1
|
||||
local try=1
|
||||
[ -n "$2" ] && try=$2
|
||||
local timeout=2
|
||||
[ -n "$3" ] && timeout=$3
|
||||
local extra_params=$4
|
||||
local repeat=$5
|
||||
local url="$1"
|
||||
local try="${2:-1}"
|
||||
local timeout="${3:-2}"
|
||||
local extra_params="$4"
|
||||
local repeat="$5"
|
||||
|
||||
if [ -z "$curl_retry_all_errors" ]; then
|
||||
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
|
||||
@@ -29,23 +27,15 @@ test_url() {
|
||||
|
||||
local max_time=$((timeout * (try + 1) + try + 3))
|
||||
curl_test() {
|
||||
/usr/bin/curl -I -o /dev/null -skL ${extra_params} --max-time ${max_time} --connect-timeout ${timeout} --retry ${try} --retry-delay 1 -w "%{http_code}" "$url"
|
||||
/usr/bin/curl -skIL -o /dev/null ${extra_params} --max-time ${max_time} --connect-timeout ${timeout} --retry ${try} --retry-delay 1 -w "%{http_code}" "$url"
|
||||
}
|
||||
|
||||
local status=$(curl_test)
|
||||
case "$status" in
|
||||
204)
|
||||
status=200
|
||||
;;
|
||||
esac
|
||||
[ "$status" = "204" ] && status=200
|
||||
if [ "$status" = "200" ] && [ "$repeat" = "1" ]; then
|
||||
sleep 3s
|
||||
status=$(curl_test)
|
||||
case "$status" in
|
||||
204)
|
||||
status=200
|
||||
;;
|
||||
esac
|
||||
[ "$status" = "204" ] && status=200
|
||||
fi
|
||||
echo $status
|
||||
}
|
||||
@@ -75,7 +65,7 @@ test_node() {
|
||||
local _type=$(echo $(config_n_get ${node_id} type) | tr 'A-Z' 'a-z')
|
||||
[ -n "${_type}" ] && {
|
||||
check_process
|
||||
local _tmp_port=$(get_new_port 48800)
|
||||
local _tmp_port=$(get_new_port 48800 tcp,udp)
|
||||
NO_REC_PROCESS=1 $APP_FILE run_socks flag="test_node_${node_id}" node=${node_id} bind=127.0.0.1 socks_port=${_tmp_port} config_file=test_node_${node_id}.json
|
||||
sleep 2s
|
||||
local curlx="socks5h://127.0.0.1:${_tmp_port}"
|
||||
@@ -92,6 +82,61 @@ test_node() {
|
||||
return 1
|
||||
}
|
||||
|
||||
try_switch_backup() {
|
||||
local b_nodes="$1"
|
||||
local now_node="$2"
|
||||
local total tried
|
||||
local new_node msg
|
||||
local first_node found node
|
||||
|
||||
# Only one backup node, alternate with the primary node.
|
||||
if [ "$backup_node_num" -eq 1 ]; then
|
||||
b_nodes="$b_nodes $main_node"
|
||||
fi
|
||||
|
||||
total=$(printf "%s\n" "$b_nodes" | wc -w)
|
||||
tried=0
|
||||
|
||||
while [ "$tried" -lt "$total" ]; do
|
||||
new_node=""
|
||||
first_node=""
|
||||
found=""
|
||||
for node in $b_nodes; do
|
||||
[ -z "$first_node" ] && first_node="$node" # Record first node.
|
||||
[ "$found" = "1" ] && { new_node="$node"; break; } # Find the current node and then get the next one.
|
||||
[ "$node" = "$now_node" ] && found=1 # flag the found current node.
|
||||
done
|
||||
# If the current node is not found, or if it is the last node, select the first node.
|
||||
[ -z "$new_node" ] && new_node="$first_node"
|
||||
|
||||
local node_role node_type node_remarks
|
||||
if [ "$new_node" = "$main_node" ]; then
|
||||
node_role="$(i18n "main node")"
|
||||
else
|
||||
node_role="$(i18n "next backup node")"
|
||||
fi
|
||||
node_type=$(config_n_get $new_node type)
|
||||
node_remarks=$(config_n_get $new_node remarks)
|
||||
log_i18n 0 "Socks switch detection: Port [%s] try %s [%s:%s]." "${socks_port}" "${node_role}" "${node_type}" "${node_remarks}"
|
||||
|
||||
if test_node ${new_node}; then
|
||||
check_process
|
||||
log_i18n 0 "Socks switch detection: Port [%s] [%s:[%s]] normal, switch to this node!" "${socks_port}" "${node_type}" "${node_remarks}"
|
||||
NO_REC_PROCESS=1 $APP_FILE socks_node_switch flag=${id} new_node=${new_node}
|
||||
[ $? -eq 0 ] && {
|
||||
log_i18n 0 "Socks switch detection: Port [%s] node switch complete!" "${socks_port}"
|
||||
}
|
||||
return 0
|
||||
fi
|
||||
log_i18n 0 "Socks switch detection: Port [%s] [%s:[%s]] abnormal." "${socks_port}" "${node_type}" "${node_remarks}"
|
||||
now_node="$new_node"
|
||||
tried=$((tried + 1))
|
||||
done
|
||||
|
||||
log_i18n 0 "Socks switch detection: Port [%s] all nodes are unavailable!" "${socks_port}"
|
||||
return 1
|
||||
}
|
||||
|
||||
test_auto_switch() {
|
||||
flag=$((flag + 1))
|
||||
local b_nodes=$1
|
||||
@@ -100,18 +145,16 @@ test_auto_switch() {
|
||||
if [ -n "$(get_cache_var "${id}")" ]; then
|
||||
now_node=$(get_cache_var "${id}")
|
||||
else
|
||||
#log_i18n 0 "Socks switch detection: Unknown error."
|
||||
#log_i18n 0 "Socks switch detection: Port [%s] Unknown error." "${socks_port}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
[ $flag -le 1 ] && {
|
||||
main_node=$now_node
|
||||
}
|
||||
|
||||
[ $flag -le 1 ] && main_node=$now_node
|
||||
|
||||
local status=$(test_proxy)
|
||||
if [ "$status" = "2" ]; then
|
||||
log_i18n 0 "Socks switch detection: Unable to connect to the network. Please check if the network is working properly!"
|
||||
log_i18n 0 "Socks switch detection: Port [%s] Unable to connect to the network. Please check if the network is working properly!" "${socks_port}"
|
||||
return 2
|
||||
fi
|
||||
|
||||
@@ -121,58 +164,23 @@ test_auto_switch() {
|
||||
[ $? -eq 0 ] && {
|
||||
check_process
|
||||
# The main node is working properly; switch to the main node.
|
||||
log_i18n 0 "Socks switch detection: Primary node 【%s: [%s]】 is normal. Switch to the primary node!" "${id}" "$(config_n_get $main_node type)" "$(config_n_get $main_node remarks)"
|
||||
$APP_FILE socks_node_switch flag=${id} new_node=${main_node}
|
||||
log_i18n 0 "Socks switch detection: Port [%s] Primary node [%s: [%s]] is normal. Switch to the primary node!" "${socks_port}" "$(config_n_get $main_node type)" "$(config_n_get $main_node remarks)"
|
||||
NO_REC_PROCESS=1 $APP_FILE socks_node_switch flag=${id} new_node=${main_node}
|
||||
[ $? -eq 0 ] && {
|
||||
log_i18n 0 "Socks switch detection: %s node switch complete!" "${id}"
|
||||
log_i18n 0 "Socks switch detection: Port [%s] node switch complete!" "${socks_port}"
|
||||
}
|
||||
return 0
|
||||
}
|
||||
fi
|
||||
|
||||
if [ "$status" = "0" ]; then
|
||||
#log_i18n 0 "Socks switch detection: %s 【%s:[%s]】 normal." "${id}" "$(config_n_get $now_node type)" "$(config_n_get $now_node remarks)"
|
||||
[ "$status" = "0" ] && {
|
||||
#log_i18n 0 "Socks switch detection: Port [%s] [%s:[%s]] normal." "${socks_port}" "$(config_n_get $now_node type)" "$(config_n_get $now_node remarks)"
|
||||
return 0
|
||||
elif [ "$status" = "1" ]; then
|
||||
local new_node msg
|
||||
if [ "$backup_node_num" -gt 1 ]; then
|
||||
# When there are multiple backup nodes
|
||||
local first_node found node
|
||||
for node in $b_nodes; do
|
||||
[ -z "$first_node" ] && first_node="$node" # Record the first node.
|
||||
[ "$found" = "1" ] && { new_node="$node"; break; } # Find the current node and then retrieve the next one.
|
||||
[ "$node" = "$now_node" ] && found=1 # Mark the current node found.
|
||||
done
|
||||
# If the current node is not found, or if the current node is the last node, then take the first node.
|
||||
[ -z "$new_node" ] && new_node="$first_node"
|
||||
local msg2="$(i18n "next backup node")"
|
||||
if [ "$new_node" = "$main_node" ]; then
|
||||
msg2="$(i18n "main node")"
|
||||
else
|
||||
[ "$now_node" = "$main_node" ] && msg2="$(i18n "backup node")"
|
||||
fi
|
||||
msg="$(i18n "switch to %s test detect!" "${msg2}")"
|
||||
else
|
||||
# When there is only one backup node, poll with the primary node.
|
||||
new_node=$([ "$now_node" = "$main_node" ] && echo "$b_nodes" || echo "$main_node")
|
||||
local msg2="$(i18n "main node")"
|
||||
[ "$now_node" = "$main_node" ] && msg2="$(i18n "backup node")"
|
||||
msg="$(i18n "switch to %s test detect!" "${msg2}")"
|
||||
fi
|
||||
log_i18n 0 "Socks switch detection: %s 【%s:[%s]】 abnormal, %s" "${id}" "$(config_n_get $now_node type)" "$(config_n_get $now_node remarks)" "${msg}"
|
||||
test_node ${new_node}
|
||||
if [ $? -eq 0 ]; then
|
||||
check_process
|
||||
log_i18n 0 "Socks switch detection: %s 【%s:[%s]】 normal, switch to this node!" "${id}" "$(config_n_get $new_node type)" "$(config_n_get $new_node remarks)"
|
||||
$APP_FILE socks_node_switch flag=${id} new_node=${new_node}
|
||||
[ $? -eq 0 ] && {
|
||||
log_i18n 0 "Socks switch detection: %s node switch complete!" "${id}"
|
||||
}
|
||||
return 0
|
||||
else
|
||||
test_auto_switch "${b_nodes}" ${new_node}
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
log_i18n 0 "Socks switch detection: Port [%s] [%s:[%s]] abnormal." "${socks_port}" "$(config_n_get $now_node type)" "$(config_n_get $now_node remarks)"
|
||||
try_switch_backup "$b_nodes" "$now_node"
|
||||
return $?
|
||||
}
|
||||
|
||||
start() {
|
||||
@@ -188,6 +196,7 @@ start() {
|
||||
backup_node=$(lua_api "get_socks_backup_nodes(\"${id}\")")
|
||||
if [ -n "$backup_node" ]; then
|
||||
backup_node_num=$(printf "%s\n" "$backup_node" | wc -w)
|
||||
log_i18n 0 "Socks switch detection: Port [%s] Number of backup nodes: %s" "${socks_port}" "${backup_node_num}"
|
||||
if [ "$backup_node_num" -eq 1 ]; then
|
||||
[ "$main_node" = "$backup_node" ] && return
|
||||
elif [ "$backup_node_num" -gt 1 ]; then
|
||||
@@ -196,6 +205,7 @@ start() {
|
||||
}
|
||||
fi
|
||||
else
|
||||
log_i18n 0 "Socks switch detection: Port [%s] Number of backup nodes: %s" "${socks_port}" "0"
|
||||
return
|
||||
fi
|
||||
while [ -n "$backup_node" ]; do
|
||||
|
||||
@@ -46,10 +46,10 @@ url_test_node() {
|
||||
local node_id=$1
|
||||
local _type=$(echo $(config_n_get ${node_id} type) | tr 'A-Z' 'a-z')
|
||||
[ -n "${_type}" ] && {
|
||||
local _tmp_port=$(get_new_port 48900)
|
||||
local _tmp_port=$(get_new_port 48900 tcp,udp)
|
||||
NO_REC_PROCESS=1 /usr/share/${CONFIG}/app.sh run_socks flag="url_test_${node_id}" node=${node_id} bind=127.0.0.1 socks_port=${_tmp_port} config_file=url_test_${node_id}.json
|
||||
sleep 2s
|
||||
local curlx="socks5h://127.0.0.1:${_tmp_port}"
|
||||
sleep 2s
|
||||
local probeUrl=$(config_n_get @global_other[0] url_test_url https://www.google.com/generate_204)
|
||||
result=$(curl --connect-timeout 3 --max-time 5 -o /dev/null -I -skL -w "%{http_code}:%{time_pretransfer}" -x ${curlx} "${probeUrl}")
|
||||
# End the SS plugin process
|
||||
|
||||
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
|
||||
FOOTSTRAP_VERSION?=
|
||||
ifneq ($(FOOTSTRAP_VERSION),)
|
||||
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
||||
PKG_RELEASE:=74
|
||||
PKG_RELEASE:=75
|
||||
endif
|
||||
|
||||
LUCI_TITLE:=Footstrap Theme
|
||||
|
||||
@@ -138,6 +138,16 @@ function clearViewIntervals() {
|
||||
/* Map, not Set: the key is the timer id and the value is what it takes to re-arm it */
|
||||
_viewIntervals.forEach((spec, id) => { if (id !== keep) window.clearInterval(id); });
|
||||
}
|
||||
/* The "Refreshing"/"Paused" pill must not outlive the poll it reports on — see the `poll-stop`
|
||||
* listener below, which is the only caller in the shipped file. Exported (fs:probe) too, because
|
||||
* navigate()'s own async chain (require(), staging into a real `.fs-content`) needs more DOM than
|
||||
* tests/lib's fakes give it; this is the one check that actually matters for the ordering bug and is
|
||||
* exercised directly, without the listener's deferral, by tests/poll-status.test.mjs. */
|
||||
function hidePollIndicatorIfEmpty() {
|
||||
if (!(L.Poll && L.Poll.queue && L.Poll.queue.length === 0)) return;
|
||||
try { ui.hideIndicator('poll-status'); }
|
||||
catch (e) { console.error('footstrap: hideIndicator threw on the poll-status teardown', e); }
|
||||
}
|
||||
/* one line per document: this runs on every navigation, and a router that cannot read L.Poll
|
||||
* cannot read it on the next click either */
|
||||
let _pollWarned = false;
|
||||
@@ -1095,6 +1105,11 @@ function navigate(pathname, push, kbd) {
|
||||
if (L.Poll && L.Poll.queue) {
|
||||
L.Poll.queue.length = 0;
|
||||
L.Poll.stop();
|
||||
/* The "Refreshing"/"Paused" pill's own teardown rides this `stop()`'s `poll-stop` event —
|
||||
* see the listener below, not a call here. A page can also empty the queue on its own, with
|
||||
* no navigation at all (`L.Poll.remove()`, e.g. `luci-mod-status`'s graphs.js on unload,
|
||||
* `luci-app-banip`'s log view), and `stop()` dispatches the same event for that call too;
|
||||
* a call placed only here would miss it, exactly the finding this once shipped without. */
|
||||
L.Poll.start();
|
||||
}
|
||||
/* kill the outgoing view's plain setInterval pollers too, as a full load would; L.Poll's own
|
||||
@@ -1401,24 +1416,42 @@ function wireRouter() {
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- the poll indicator must not outlive the poll ----
|
||||
/* ---- the poll-status pill must not outlive the poll it reports on ----
|
||||
*
|
||||
* LuCI shows the "Refreshing" pill on `poll-start`, flips it to "Paused" on `poll-stop` and never
|
||||
* hides it again (core calls ui.hideIndicator() only for `uci-changes`). That is invisible on a full
|
||||
* load, since Poll.start() dispatches `poll-start` only for a non-empty queue — but this router
|
||||
* flushes the queue and calls stop() on every navigation, so walking from a polled page to an
|
||||
* unpolled one leaves a "Paused" pill reporting on a poll that does not exist. The pill exists iff
|
||||
* there is something to poll. Registered at module eval, i.e. after luci.js's own listener, so this
|
||||
* runs second. */
|
||||
* LuCI shows the pill on `poll-start`, flips it to "Paused" on `poll-stop`, and never hides it
|
||||
* again. Two different callers empty the queue and reach `L.Poll.stop()`: this router's own
|
||||
* navigate() teardown, and a VIEW emptying its own queue mid-page with no navigation at all
|
||||
* (`L.Poll.remove()` — `luci-mod-status`'s Realtime Graphs on unload, `luci-app-banip`'s log
|
||||
* template) — both dispatch the same `poll-stop`, so one listener here covers both, where a call
|
||||
* placed only in navigate()'s teardown covered the first and silently missed the second: reproduced
|
||||
* live, owrt2410b and owrt2512b, add-then-remove on Statistics — `{"active":false,"q":0,"pill":
|
||||
* {"text":"Paused","clickable":true}}`, unchanged by a click, because nothing had ever asked to hide
|
||||
* a pill for a poll that a `remove()` rather than a navigation had ended.
|
||||
*
|
||||
* The HIDE itself is deferred past the synchronous dispatch, one microtask, rather than run inline
|
||||
* in this listener: `stop()` dispatches `poll-stop` synchronously to every listener registered at
|
||||
* that moment, including luci.js's own (`showIndicator('poll-status', 'Paused', null, 'inactive')` —
|
||||
* "Paused" is always shown with `handler: null`), and which of the two ran first here used to decide
|
||||
* everything — luci.js registers its listener from `setupDOM()`, after an async chain
|
||||
* (DOMContentLoaded + ui/rpc/form + probeRPCBaseURL), this module at eval, from the inline
|
||||
* `L.require('menu-footstrap')` in `partials/footer.ut`, so network/cache timing picked the order.
|
||||
* Run with this listener first, its hide removed the span before luci.js re-created it for "Paused"
|
||||
* with no handler, and the next `poll-start` found that span already there and only changed its
|
||||
* text — clickless for the rest of the document. A microtask runs only once the whole synchronous
|
||||
* turn that dispatched the event has unwound, i.e. after every `poll-stop` listener already fired
|
||||
* regardless of which ran first, so the check sees the state stock's listener actually left rather
|
||||
* than racing it. It still reads `L.Poll.queue.length` at that later moment, not at dispatch time, so
|
||||
* a queue the SAME navigation's incoming page has already refilled by then is left alone — the
|
||||
* incoming page's own `require()`/render() is real async work (network, RPCs), always later than a
|
||||
* microtask queued during the synchronous `stop()` call that preceded it. `tests/poll-status.test.mjs`
|
||||
* drives both paths (a navigation's queue flush and a bare `Poll.remove()` to empty) in both listener
|
||||
* orders. docs/spa-router.md, "Teardown". */
|
||||
document.addEventListener('poll-stop', () => {
|
||||
if (L.Poll && L.Poll.queue && L.Poll.queue.length === 0) {
|
||||
try { ui.hideIndicator('poll-status'); }
|
||||
catch (e) { console.error('footstrap: hideIndicator threw on poll-stop', e); }
|
||||
}
|
||||
queueMicrotask(hidePollIndicatorIfEmpty);
|
||||
});
|
||||
|
||||
/* At module eval, like the listener above: the session can die during the first view's own data
|
||||
* calls, before anything has called wire(), and an interceptor registered later never sees it. */
|
||||
/* At module eval: the session can die during the first view's own data calls, before anything has
|
||||
* called wire(), and an interceptor registered later never sees it. */
|
||||
watchSession();
|
||||
|
||||
/* Pause LuCI's 1 s poll loop while the tab is hidden: LuCI has no visibilitychange handler, so an
|
||||
@@ -1470,6 +1503,10 @@ return baseclass.extend({
|
||||
* navigate() is the real caller of the first and `_expired` gates the second. */
|
||||
clearViewIntervals, /* fs:probe */
|
||||
sessionExpired, /* fs:probe */
|
||||
/* tests/poll-status.test.mjs: the deferred `poll-stop` listener's own check, called directly so a
|
||||
* test need not also fake a microtask tick to see it fire (see the comment at its definition and
|
||||
* at the listener above) */
|
||||
hidePollIndicatorIfEmpty, /* fs:probe */
|
||||
/* fs-search warms its recents and the arrow-key-highlighted result, neither of which the
|
||||
* pointer/focus triggers above can see. The edge points search -> router, because the router
|
||||
* must keep no dependency on the palette. */
|
||||
|
||||
@@ -258,13 +258,21 @@
|
||||
}
|
||||
/* The frame is overflow:visible, so nothing clips the square corner cells to its radius and
|
||||
* they poked past it. Round the four OUTER cells — the frame's corners, not a per-row pill.
|
||||
* `#view .table[id]` is listed because the frame rule draws the border there too. */
|
||||
.cbi-section .table[id] .tr.table-titles .th:first-child,
|
||||
#view .table[id] .tr.table-titles .th:first-child,
|
||||
table.fs-dt .tr.table-titles .th:first-child { border-top-left-radius: var(--fs-radius-lg); }
|
||||
.cbi-section .table[id] .tr.table-titles .th:last-child,
|
||||
#view .table[id] .tr.table-titles .th:last-child,
|
||||
table.fs-dt .tr.table-titles .th:last-child { border-top-right-radius: var(--fs-radius-lg); }
|
||||
* `#view .table[id]` is listed because the frame rule draws the border there too.
|
||||
*
|
||||
* `:is(.tr.table-titles, .thead)` because a header is any of the shapes fs-select.js adopts:
|
||||
* `.tr.table-titles` is the bare-th-row markup, `.thead` covers a real `<thead>` whether it
|
||||
* holds its `<th>`s directly (E()-built, no `<tr>`) or through one — adoptMarkup() never adds
|
||||
* `.tr` to that inner row, so a class-only selector has to reach the `.th` by descent, not by
|
||||
* child. Without `.thead` the header kept its square top corners under any real `<thead>`: the
|
||||
* DHCP and Wi-Fi dashboard tables measured a first/last `.th` radius of 0 against the frame's
|
||||
* 12px. */
|
||||
.cbi-section .table[id] :is(.tr.table-titles, .thead) .th:first-child,
|
||||
#view .table[id] :is(.tr.table-titles, .thead) .th:first-child,
|
||||
table.fs-dt :is(.tr.table-titles, .thead) .th:first-child { border-top-left-radius: var(--fs-radius-lg); }
|
||||
.cbi-section .table[id] :is(.tr.table-titles, .thead) .th:last-child,
|
||||
#view .table[id] :is(.tr.table-titles, .thead) .th:last-child,
|
||||
table.fs-dt :is(.tr.table-titles, .thead) .th:last-child { border-top-right-radius: var(--fs-radius-lg); }
|
||||
.cbi-section .table[id] .tr:last-child .td:first-child,
|
||||
#view .table[id] .tr:last-child .td:first-child,
|
||||
table.fs-dt .tr:last-child .td:first-child { border-bottom-left-radius: var(--fs-radius-lg); }
|
||||
@@ -473,6 +481,19 @@
|
||||
.table .tr.cbi-section-table-titles::before { border-bottom: var(--fs-hairline); }
|
||||
.table .tr:not(.table-titles):not(.cbi-section-table-titles):not(:last-child) > .td,
|
||||
.table .tr.cbi-section-table-row:not(:last-child)::before { border-bottom: var(--fs-hairline); }
|
||||
/* Because the separator above is a per-CELL border, an app that nudges one cell with
|
||||
* `position: relative; top: …` (luci-mod-dashboard's `.devices-info .td:nth-child(3)`) drags
|
||||
* that cell's border-bottom out of line with its row neighbours — a step, not a slanted line,
|
||||
* since each cell paints its own hairline. Bootstrap never shows this because its rows are
|
||||
* `border-collapse: collapse`, with no per-cell border to displace. The app's rule lands in the
|
||||
* same fenced `theme` layer (fs-sheets.js), so an ID root outranks its 7 classes on specificity
|
||||
* alone and needs no `!important`; `top` AND `bottom`, not just `top: 0`, so this also cancels
|
||||
* a `bottom` offset and leaves `position: relative` itself untouched for any
|
||||
* absolutely-positioned descendant. `top`/`bottom`, not the `inset-block` shorthand: the sheet
|
||||
* has never used a logical inset property before and `css-floor` has no support decision for
|
||||
* one, where the physical pair is already on the floor. Measured: offset .td top -3px -> 0, its
|
||||
* border-bottom back level with its neighbours'. */
|
||||
:is(#view, #modal_overlay) .fs-dt .td { top: auto; bottom: auto; }
|
||||
/* descendant combinator (not >) so a real <table>'s browser-inserted <tbody> is covered
|
||||
* too — one rule themes every data table, div- or table-based. */
|
||||
.table .tr.table-titles, .table .tr.cbi-section-table-titles {
|
||||
|
||||
@@ -138,11 +138,19 @@
|
||||
/* A raw checkbox inside a button-styled label sat flush against its text: base zeroes the
|
||||
* checkbox margin (30-forms.css) and only a literal space separated the ✓ from the words
|
||||
* (issue #5.1 — attendedsysupgrade's "Keep settings" confirm: label.btn > input[type=checkbox]
|
||||
* + text). Restore a gap. The toggle-switch .cbi-checkbox below is unaffected — its input is
|
||||
* opacity:0, so this margin moves nothing there. */
|
||||
* + text). Restore a gap. The toggle-switch .cbi-checkbox below is unaffected — its own
|
||||
* margin: 0 outranks this rule by specificity ((0,2,1) vs this rule's (0,2,1) on a DIFFERENT
|
||||
* ancestor class, so the two never both match one element: `.cbi-checkbox` is never `.btn`). */
|
||||
.btn > input[type="checkbox"], .btn > input[type="radio"] { margin-inline-end: .5em; }
|
||||
|
||||
/* checkbox -> toggle switch
|
||||
/* checkbox -> toggle switch, drawn on the INPUT itself.
|
||||
*
|
||||
* Was drawn on label[for] with the real input hidden (position:absolute; opacity:0; width/
|
||||
* height:0) — forum.openwrt.org/t/251930 post #141: luci-app-modemdata (and any third-party CSS
|
||||
* built the way bootstrap expects, styling the native input) injects an unlayered
|
||||
* `label[for] { display: none }` flagged important, scoped to its own markup, which left
|
||||
* NOTHING visible, since the input it left alone was the invisible one. Drawing on the input
|
||||
* means an app that hides label[for] hides an already-empty placeholder, not the switch.
|
||||
*
|
||||
* The pill, the knob and the knob's travel are all on --fs-density-box. As bare literals they
|
||||
* make LuCI's most common control the only one in the theme that ignores the Density axis, while
|
||||
@@ -154,25 +162,19 @@
|
||||
*
|
||||
* flex-shrink: 0 below (task 0145): `.cbi-checkbox` is the flex row, and a caption after the
|
||||
* pill — `label.cbi-checkbox > input + label[for] + TEXT`, package-manager.js's and banip's real
|
||||
* markup, not the caption-less div the gallery had — makes the pill a SECOND, shrinkable flex
|
||||
* item. Unshrunk it stays exactly --sw-w; measured shrunk on the live Install dialog on both
|
||||
* stands, RU/Normal/390px gave a used width of 30.17px against 40 declared and a knob 6.83px past
|
||||
* the pill's right edge, EN/Large/360px 36.22 against 46 declared and a 6.47px overhang —
|
||||
* flex-shrink: 0 alone took both to -3.00px and -3.31px (the knob safely inside the pill, not
|
||||
* flush with it), so a second guard (e.g. min-width: var(--sw-w), redundant with a non-shrinking
|
||||
* flex-basis) was not needed and does not ship.
|
||||
* markup — makes the input (now the real, sized flex item; label[for] no longer is one, below) a
|
||||
* SECOND, shrinkable flex item. Unshrunk it stays exactly --sw-w; measured shrunk on the live
|
||||
* Install dialog on both stands when this box was still label[for]'s (task 0145): RU/Normal/390px
|
||||
* gave a used width of 30.17px against 40 declared and a knob 6.83px past the pill's right edge,
|
||||
* EN/Large/360px 36.22 against 46 declared and a 6.47px overhang — flex-shrink: 0 alone took both
|
||||
* to -3.00px and -3.31px (the knob safely inside the pill, not flush with it). The flex row and
|
||||
* its sibling caption are unchanged by which element carries the pill, so the guard carries over.
|
||||
*
|
||||
* Scoped to `.cbi-checkbox > label[for]`, not a bare `.cbi-checkbox > label`: ui.js's
|
||||
* Checkbox.render() appends a SECOND label when `tooltip` is set (form.Flag.tooltip — e.g.
|
||||
* luci-app-firewall's zones.js on `masq`), `label.cbi-tooltip-container` holding the hint icon,
|
||||
* and the bare selector painted it as a second switch too — measured on the gallery's tooltip
|
||||
* fixture, both labels 40x22 at rgb(55,62,71), flush against each other with no gap. `[for]` is
|
||||
* the discriminator rather than `input[type="checkbox"] + label`: Checkbox.render() always
|
||||
* builds the switch label as `E('label', { 'for': id })` and the tooltip label with no `for` at
|
||||
* all, so the attribute alone tells the two apart, at a fifth of the bytes — the sibling
|
||||
* combinator repeated across five rules (this one, its ::before/::after, and the two
|
||||
* forced-colors rules in 95-a11y-media.css) put cascade.css 186 B over its budget; rescoping
|
||||
* dropped it back under. */
|
||||
* label[for] stays in the DOM (ui.js and every third-party app that builds this markup emit it)
|
||||
* but draws and occupies nothing: `[for]` still discriminates it from the SECOND label
|
||||
* Checkbox.render() appends when `tooltip` is set (form.Flag.tooltip — e.g. luci-app-firewall's
|
||||
* zones.js on `masq`), `label.cbi-tooltip-container` holding the hint icon, which must keep
|
||||
* rendering. */
|
||||
/* `overflow-wrap: anywhere`: the caption after the pill is a plain text flex item, which already
|
||||
* wraps at spaces on its own — this is only for the one WORD that does not fit even at the row's
|
||||
* own full width. Measured on this exact caption (see the pill's own comment below): pseudo-loc
|
||||
@@ -181,31 +183,45 @@
|
||||
* is not implausible (the theme already ships an untranslated one), so the row should absorb it
|
||||
* by breaking the word, not by escaping its column. */
|
||||
.cbi-checkbox { display: inline-flex; align-items: center; gap: var(--fs-space-2); overflow-wrap: anywhere; }
|
||||
.cbi-checkbox > input[type="checkbox"] { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; }
|
||||
.cbi-checkbox > label[for] {
|
||||
/* label[for] takes over the input's old trick (position:absolute, zero box, opacity:0): out of
|
||||
* flow and out of the flex item count, so `.cbi-checkbox`'s `gap` still opens on only the SAME
|
||||
* two neighbours it always did — see the tooltip-container note below. */
|
||||
.cbi-checkbox > label[for] { position: absolute; opacity: 0; width: 0; height: 0; margin: 0; }
|
||||
.cbi-checkbox > input[type="checkbox"] {
|
||||
--sw-w: calc(40px * var(--fs-density-box));
|
||||
--sw-h: calc(22px * var(--fs-density-box));
|
||||
--sw-knob: calc(16px * var(--fs-density-box));
|
||||
--sw-pad: calc(2px * var(--fs-density-box));
|
||||
/* `appearance: none` and this compound's other unstated properties (color, the invalid
|
||||
* --bd-color/--fg-color custom props) still come from the bare `input[type="checkbox"]`
|
||||
* rule further down, written for a third-party app's raw markup: harmless to share, since
|
||||
* this input never reads --bd-color/--fg-color. The parts of that rule that WOULD paint
|
||||
* over this switch (the drawn box, the mark, the focus ring) are scoped with
|
||||
* `:not(:where(.cbi-checkbox > input))` at their own selector instead of being undone here —
|
||||
* cheaper than restating every property this rule already states for the opposite reason,
|
||||
* and it is one property, not eight, that both rules would otherwise fight over. */
|
||||
display: inline-block; position: relative; margin: 0; flex-shrink: 0;
|
||||
width: var(--sw-w); height: var(--sw-h); border-radius: var(--fs-radius-pill);
|
||||
background: var(--fs-track); border: var(--fs-hairline);
|
||||
cursor: pointer; transition: background var(--fs-dur), border-color var(--fs-dur); vertical-align: middle;
|
||||
}
|
||||
/* The input is opacity:0 at 0x0, so this label IS the whole hit box of LuCI's most common
|
||||
* control — and at 22px tall it sits under the 24px floor of WCAG 2.5.8. The floor is about the
|
||||
* CLICKABLE area, not the drawing: an invisible overlay stretches the target to 44x30 without
|
||||
* fattening the pill (a visual 44x24 was tried and looked bloated). position:absolute, so it
|
||||
* adds nothing to layout; the knob keeps ::after.
|
||||
/* The invisible overlay stretching the hit target to 44x30 (WCAG 2.5.8: the pill is 22px tall,
|
||||
* under the 24px floor) — moved here from label[for] unchanged. position:absolute, so it adds
|
||||
* nothing to layout; the knob keeps ::after.
|
||||
*
|
||||
* `content`/`position` come from the raw checkbox's own shared `::before, ::after { position:
|
||||
* absolute; content: "" }` rule further down — left UNSCOPED and reused here rather than
|
||||
* restated, since both pseudo-elements need exactly those two values anyway. The two rules
|
||||
* that would otherwise paint a drawn box and a mark ON this pseudo (their own selectors,
|
||||
* further down) are scoped with `:not(:where(.cbi-checkbox > input))` instead, so nothing is left to
|
||||
* override here.
|
||||
*
|
||||
* The inset stays literal while the pill above scales: this is an accessibility floor, and a
|
||||
* floor that shrinks with a density setting is not one. At Compact the target is still 38x26.7,
|
||||
* over the minimum. */
|
||||
.cbi-checkbox > label[for]::before {
|
||||
content: ""; position: absolute; inset: -4px -2px;
|
||||
}
|
||||
.cbi-checkbox > label[for]::after {
|
||||
content: ""; position: absolute; top: var(--sw-pad); inset-inline-start: var(--sw-pad);
|
||||
.cbi-checkbox > input[type="checkbox"]::before { inset: -4px -2px; }
|
||||
.cbi-checkbox > input[type="checkbox"]::after {
|
||||
top: var(--sw-pad); inset-inline-start: var(--sw-pad);
|
||||
width: var(--sw-knob); height: var(--sw-knob); border-radius: 50%;
|
||||
background: var(--fs-dim);
|
||||
/* transform, not `left`: `left` is a layout property, so the knob re-laid out the row on
|
||||
@@ -213,47 +229,58 @@
|
||||
transform: translateX(0);
|
||||
transition: transform var(--fs-dur-move), background var(--fs-dur);
|
||||
}
|
||||
/* The tooltip-container label (above) is now unstyled by the switch rules, so it falls back to
|
||||
* the browser's plain inline label plus base's `cursor: help` — the icon glyph, no pill. It still
|
||||
/* The tooltip-container label (above) is unstyled by the switch rules and falls back to the
|
||||
* browser's plain inline label plus base's `cursor: help` — the icon glyph, no pill. It still
|
||||
* needs a gap from the switch, and `.cbi-checkbox`'s own `gap` above gives it one instead of a
|
||||
* margin declared here: the checkbox input inside that flex row is `position: absolute`, so it is
|
||||
* never a flex item, and `gap` only ever opens BETWEEN flex items — an ordinary `.cbi-checkbox`
|
||||
* with just the switch label has one flex item and gets no gap at all (measured: cb1's box is
|
||||
* still 40px wide, unchanged), while the tooltip fixture's two labels get exactly one. --fs-space-2
|
||||
* (8px), same number the removed margin used — the gap the theme already uses beside an inline
|
||||
* checkbox glyph (`.btn > input[type="checkbox"]` above) — measured on the gallery fixture: top
|
||||
* edges 0.5px apart (the icon glyph's own line box, not this gap) and left-to-right edge 8px,
|
||||
* both unchanged from before this rescope. */
|
||||
.cbi-checkbox > input[type="checkbox"]:checked + label { background: var(--fs-accent); border-color: transparent; }
|
||||
* margin declared here: label[for] inside that flex row is `position: absolute` (above), so it
|
||||
* is never a flex item, and `gap` only ever opens BETWEEN flex items — an ordinary `.cbi-checkbox`
|
||||
* with just the switch input has one flex item (the input itself) and gets no gap at all, while
|
||||
* the tooltip fixture's input+tooltip-label pair gets exactly one. --fs-space-2 (8px), same
|
||||
* number the removed margin used — the gap the theme already uses beside an inline checkbox
|
||||
* glyph (`.btn > input[type="checkbox"]` above). The flex-item COUNT and the gap token are the
|
||||
* same as before this rule moved from label[for] to the input; only which element is the
|
||||
* absolute one swapped. */
|
||||
.cbi-checkbox > input[type="checkbox"]:checked { background: var(--fs-accent); border-color: transparent; }
|
||||
/* The travel is derived rather than hand-written, so it cannot drift out of step with the three
|
||||
* sizes above. The `- 2px` is the pill's two 1px BORDERS and is the whole subtlety: --sw-w is a
|
||||
* border-box width, while an absolutely-positioned ::after is placed against the PADDING box, so
|
||||
* the knob has 2px less room and deriving without that term lands it off-centre — the travel
|
||||
* comes out 20px where symmetry wants 18. The border does not scale with density, so the term
|
||||
* stays literal. */
|
||||
.cbi-checkbox > input[type="checkbox"]:checked + label::after {
|
||||
.cbi-checkbox > input[type="checkbox"]:checked::after {
|
||||
transform: translateX(calc(var(--sw-w) - var(--sw-knob) - var(--sw-pad) * 2 - 2px));
|
||||
background: var(--fs-on-accent);
|
||||
}
|
||||
/* The real checkbox is opacity:0 at 0x0 above, so base's `input:focus::before` glow paints on a
|
||||
* 0x0 box. The <label> IS the switch, so the ring goes there, or the most common control in LuCI
|
||||
* shows nothing under keyboard focus.
|
||||
/* The bare fallback ring further down (`input:focus`, for every untyped/unlisted input, and its
|
||||
* `input[type="checkbox"]:focus::before` twin) is scoped with `:not(:where(.cbi-checkbox > input))` at
|
||||
* its own selector, so a plain click on this real, visible input rings nothing — as it never
|
||||
* did before this input was the drawn element, since only :focus-visible below is meant to. */
|
||||
/* The <input> IS the switch now, so the ring goes there directly, or the most common control in
|
||||
* LuCI shows nothing under keyboard focus.
|
||||
*
|
||||
* -solo, because on the CHECKED switch — half the switches on any page — neither other channel
|
||||
* exists: the rule above fills the track with --fs-accent and sets `border-color: transparent`,
|
||||
* so the border flip is accent on accent (measured: fill rgb(86,157,245) against a border of the
|
||||
* same accent at .97 alpha) and the tinted ring is accent at .15 alpha. */
|
||||
.cbi-checkbox > input[type="checkbox"]:focus-visible + label {
|
||||
.cbi-checkbox > input[type="checkbox"]:focus-visible {
|
||||
border-color: var(--fs-accent);
|
||||
box-shadow: var(--fs-focus-ring-solo);
|
||||
}
|
||||
.cbi-checkbox > input[type="checkbox"]:disabled + label { opacity: var(--fs-disabled-opacity); cursor: not-allowed; }
|
||||
.cbi-checkbox > input[type="checkbox"]:disabled { opacity: var(--fs-disabled-opacity); cursor: not-allowed; }
|
||||
|
||||
/* Raw checkbox/radio — the drawing, absorbed from base whole: it undoes the generic `input` box
|
||||
* below, and left in base every one of those overrides would lose to that rule by layer. The box
|
||||
* is 16px on the grid and the tick or dot is inset FROM it rather than restating its own number,
|
||||
* so resizing the box cannot leave the mark off-centre. LuCI's own forms wrap checkboxes in the
|
||||
* .cbi-checkbox toggle above; the raw drawing is what a third-party app's bare markup gets. */
|
||||
* .cbi-checkbox toggle above; the raw drawing is what a third-party app's bare markup gets.
|
||||
*
|
||||
* `.cbi-checkbox > input` matches the switch's own input too: this base rule stays unscoped for
|
||||
* it, since the switch's own rules above want its `appearance: none` and read neither
|
||||
* --bd-color nor --fg-color. But the drawn box, the mark and the focus ring below DO paint over
|
||||
* the switch once its input is a real, visible element rather than 0x0 — each of those three
|
||||
* selectors carries `:not(:where(.cbi-checkbox > input))` for that reason alone; undoing them instead,
|
||||
* property by property, on the switch's own rules costs more than the one exclusion clause
|
||||
* each needs. */
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
--bd-color: var(--fs-border);
|
||||
@@ -307,7 +334,7 @@
|
||||
position: absolute;
|
||||
content: "";
|
||||
}
|
||||
input[type="checkbox"]::before,
|
||||
input[type="checkbox"]:not(:where(.cbi-checkbox > input))::before,
|
||||
input[type="radio"]::before {
|
||||
/* centred in the 24px target, and direction-agnostic: `inset: 0; margin: auto` on a box with
|
||||
* a fixed size is the one centring that needs no translate and therefore behaves the same in
|
||||
@@ -333,7 +360,7 @@
|
||||
* of ringing a clicked box. Both states (plain and invalid) are declared once, further
|
||||
* down this file, in the selector lists every other same-body ring joins — see the two
|
||||
* `--fs-focus-ring`/`--fs-focus-ring-invalid` rules below the text-input group. */
|
||||
input[type="checkbox"]::after,
|
||||
input[type="checkbox"]:not(:where(.cbi-checkbox > input))::after,
|
||||
input[type="radio"]::after {
|
||||
/* the mark is inset from the DRAWING, and the drawing is centred in the target, so the mark
|
||||
* centres the same way rather than measuring from the target's own edge */
|
||||
@@ -342,7 +369,7 @@
|
||||
width: calc(var(--box) - var(--mark-inset) * 2);
|
||||
height: calc(var(--box) - var(--mark-inset) * 2);
|
||||
}
|
||||
input[type="checkbox"]:checked::after,
|
||||
input[type="checkbox"]:not(:where(.cbi-checkbox > input)):checked::after,
|
||||
input[type="radio"]:checked::after {
|
||||
/* OURS. Drawn as a STROKE, which is this theme's icon language (see --spinner-icon in
|
||||
* base/95-luci.css and --fs-icon-refresh in 02-tokens.css: fill:none, round caps, a 24 grid).
|
||||
@@ -626,15 +653,16 @@
|
||||
textarea:hover { border-color: var(--fs-dim); }
|
||||
|
||||
/* bare `input:focus` absorbed from base's generic glow onto the same ring: it is the fallback
|
||||
* for the types the list does not name and for a typeless <input>; on checkbox/radio it lands
|
||||
* on the invisible element box UNDER the drawn ::before ring above — coincident boxes, one
|
||||
* visible ring, as the old glow behaved. `select:focus` and the keyboard-removable dynlist
|
||||
* chip (ui.js gives it a tabindex) join here rather than repeating the same three
|
||||
* declarations under their own selector: one shared body instead of three, 149 B off
|
||||
* cascade.css. */
|
||||
* for the types the list does not name and for a typeless <input>; on a bare checkbox/radio it
|
||||
* lands on the drawn box UNDER the ::before ring below — coincident boxes, one visible ring, as
|
||||
* the old glow behaved. `:not(:where(.cbi-checkbox > input))` excludes the toggle switch, whose input is
|
||||
* a real, visible element with its own :focus-visible-only ring (above) — a plain click on it
|
||||
* must ring nothing. `select:focus` and the keyboard-removable dynlist chip (ui.js gives it a
|
||||
* tabindex) join here rather than repeating the same three declarations under their own
|
||||
* selector: one shared body instead of three, 149 B off cascade.css. */
|
||||
.cbi-input-text:focus, .cbi-input-password:focus, .cbi-input-textarea:focus,
|
||||
.cbi-section-create-name:focus,
|
||||
input:focus,
|
||||
input:focus:not(:where(.cbi-checkbox > input)),
|
||||
input[type="text"]:focus, input[type="password"]:focus, input[type="number"]:focus,
|
||||
input[type="email"]:focus, input[type="url"]:focus, input[type="search"]:focus,
|
||||
input[type="tel"]:focus, input[type="date"]:focus, input[type="time"]:focus,
|
||||
@@ -651,8 +679,9 @@
|
||||
* off cascade.css. `:is()` around the `:has()` compound keeps the list forgiving: unwrapped, a
|
||||
* `:has()` compound sharing a selector list with plain ones fails the WHOLE rule below the
|
||||
* floor (Firefox < 121; docs/css.md "Two shapes fail it"), instead of just losing that one
|
||||
* selector. */
|
||||
input[type="checkbox"]:focus::before, input[type="radio"]:focus::before,
|
||||
* selector. `:not(:where(.cbi-checkbox > input))` on the checkbox part for the same reason as the plain
|
||||
* ring above: the toggle switch's own :focus-visible rule is the only one allowed to ring it. */
|
||||
input[type="checkbox"]:not(:where(.cbi-checkbox > input)):focus::before, input[type="radio"]:focus::before,
|
||||
.cbi-select.focus,
|
||||
.cbi-dynlist > .add-item:focus-within,
|
||||
.control-group:is(:has(> .cbi-input-password)):focus-within {
|
||||
|
||||
@@ -116,17 +116,18 @@
|
||||
padding-inline-start: calc(var(--fs-space-3) - 3px);
|
||||
}
|
||||
/* the toggle switch is a bare rounded box in forced colors — give the knob and
|
||||
* the track an edge so on/off is distinguishable at all. Scoped via `label[for]`, matching
|
||||
* 60-inputs.css: a bare `.cbi-checkbox > label` also reaches ui.Checkbox's `tooltip` label
|
||||
* (the hint-icon one, unstyled otherwise, and built with no `for` attribute), which would
|
||||
* grow the same border a switch does not own. */
|
||||
.cbi-checkbox > label[for] { border: 1px solid ButtonBorder; }
|
||||
.cbi-checkbox > input[type="checkbox"]:checked + label {
|
||||
* the track an edge so on/off is distinguishable at all. Scoped via `input[type="checkbox"]`,
|
||||
* matching 60-inputs.css, which now draws the switch there: an `<input>` selector cannot
|
||||
* reach ui.Checkbox's `tooltip` label (the hint-icon one, unstyled otherwise, built with no
|
||||
* `for` attribute) by construction, so the old `[for]` discriminator is no longer needed to
|
||||
* keep that label from growing a border it does not own. */
|
||||
.cbi-checkbox > input[type="checkbox"] { border: 1px solid ButtonBorder; }
|
||||
.cbi-checkbox > input[type="checkbox"]:checked {
|
||||
background: Highlight;
|
||||
forced-color-adjust: none;
|
||||
}
|
||||
.cbi-checkbox > label[for]::after { background: ButtonBorder; }
|
||||
.cbi-checkbox > input[type="checkbox"]:checked + label::after { background: HighlightText; }
|
||||
.cbi-checkbox > input[type="checkbox"]::after { background: ButtonBorder; }
|
||||
.cbi-checkbox > input[type="checkbox"]:checked::after { background: HighlightText; }
|
||||
/* icon-only buttons (rail toggle, search/appearance, logout) lose their hover fill */
|
||||
.fs-railtoggle, .fs-themerow { border-color: ButtonBorder; }
|
||||
/* the focus ring is a box-shadow everywhere in this theme, and box-shadow is
|
||||
|
||||
+2
-2
@@ -5,12 +5,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=rtl8189es
|
||||
PKG_RELEASE:=5
|
||||
PKG_RELEASE:=6
|
||||
|
||||
PKG_SOURCE_URL:=https://github.com/jwrdegoede/rtl8189ES_linux.git
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_DATE:=2025-09-27
|
||||
PKG_SOURCE_VERSION:=f4712611578312fcd67795a0d43d78eb70378238
|
||||
PKG_SOURCE_VERSION:=0a5d04114fac3c9f48a343cb905fbb6a3f9f5df5
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
|
||||
Reference in new issue
Block a user