🎨 Sync 2026-11-12 18:00:39

This commit is contained in:
github-actions[bot] committed 2026-11-12 18:00:39 +08:00
1 parent 47f4b4d6ab
commit ed73e42a08
27 files changed
+1828 -1574

No files matched your search

+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=gately PKG_NAME:=gately
PKG_VERSION:=0.6.5.5 PKG_VERSION:=0.6.6
PKG_RELEASE:=32 PKG_RELEASE:=33
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)?
+3 -3
View File
@@ -5,11 +5,11 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=honk PKG_NAME:=honk
PKG_VERSION:=2026.11.09~6484888 PKG_VERSION:=2026.11.12~a09d3bf
PKG_RELEASE:=34 PKG_RELEASE:=35
HONK_SRC_URL:=https://github.com/daeuniverse/honk.git HONK_SRC_URL:=https://github.com/daeuniverse/honk.git
HONK_SRC_VERSION:=64848881b7304e250e7a25422f4f1d86a5975eca HONK_SRC_VERSION:=a09d3bfa272f3670d6606373452be41606bdc29e
PKG_LICENSE:=AGPL-3.0-only PKG_LICENSE:=AGPL-3.0-only
PKG_LICENSE_FILES:=LICENSE PKG_LICENSE_FILES:=LICENSE
+2 -4
View File
@@ -10,10 +10,8 @@ LUCI_DEPENDS:=+fastnet +luci-lib-linkeaseauth +linkease-app-entry
# APK uses -rN; OPKG uses -N for runtime package releases. # APK uses -rN; OPKG uses -N for runtime package releases.
LUCI_EXTRA_DEPENDS:=fastnet (>=0.8.9-$(if $(CONFIG_USE_APK),r)1) LUCI_EXTRA_DEPENDS:=fastnet (>=0.8.9-$(if $(CONFIG_USE_APK),r)1)
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_VERSION:=0.8.9-r1 PKG_VERSION:=0.8.9-r2
PKG_RELEASE:=8 PKG_RELEASE:=9
# Preserve LuCI translation version ordering when upgrading older git-* packages.
PKG_PO_VERSION:=git-26.313.00000-082
LUCI_MINIFY_CSS:=0 LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0 LUCI_MINIFY_JS:=0
@@ -490,42 +490,39 @@ function flagToBool(flag) {
function renderPayload(s, total, uciconfig) { function renderPayload(s, total, uciconfig) {
// common payload // common payload
let initPayload = function(o, n, key) { const initPayload = function(o, n, key,
load_callback = (n, k, inst) => inst.getPayload(n)[k],
onchange_callback = (n, k, v, inst) => inst.setPayload(n, Object.fromEntries([[k, v]]))
) {
o.load = function(section_id) { o.load = function(section_id) {
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key]; return load_callback(n, key, this.section.getOption('entry').instget(section_id));
} }
o.onchange = function(ev, section_id, value) { o.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'entry');
let n = this.option.match(/^payload(\d+)_/)[1]; let n = this.option.match(/^payload(\d+)_/)[1];
let rule = new RulesEntry(UIEl.getValue()).setPayload(n, {factor: value}); this.section.getOption('entry').instset(section_id, L.bind(onchange_callback, this, n, key, value));
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
} }
o.write = function() {}; o.write = function() {};
o.rmempty = false; o.rmempty = false;
o.modalonly = true; o.modalonly = true;
} }
let initDynamicPayload = function(o, n, key) { const initDynamicPayload = function(o, n, key,
load_callback = (n, k, inst) => inst.getPayloads().slice(n).map(e => e[k] ?? ''),
onchange_callback = (n, k, v, inst) => {
const limit = inst.getPayloads().length;
v.forEach((val) => {
inst.setPayload(n, Object.fromEntries([[k, val]])); n++;
});
inst.setPayload(limit, Object.fromEntries([[k, null]]), limit);
}
) {
o.allowduplicates = true; o.allowduplicates = true;
o.load = function(section_id) { o.load = function(section_id) {
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayloads().slice(n).map(e => e[key] ?? ''); return load_callback(n, key, this.section.getOption('entry').instget(section_id));
} }
o.validate = function(section_id, value) { o.validate = function(section_id, value) {
value = this.formvalue(section_id); value = this.formvalue(section_id);
let UIEl = this.section.getUIElement(section_id, 'entry');
let rule = new RulesEntry(UIEl.getValue());
let n = this.option.match(/^payload(\d+)_/)[1]; let n = this.option.match(/^payload(\d+)_/)[1];
let limit = rule.getPayloads().length; this.section.getOption('entry').instset(section_id, L.bind(onchange_callback, this, n, key, value));
value.forEach((val) => {
rule.setPayload(n, {factor: val}); n++;
});
rule.setPayload(limit, {factor: null}, limit);
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
return true; return true;
} }
@@ -535,6 +532,7 @@ function renderPayload(s, total, uciconfig) {
} }
let o, prefix; let o, prefix;
// StaticList payload // StaticList payload
for (let n=0; n<total; n++) { for (let n=0; n<total; n++) {
prefix = `payload${n}_`; prefix = `payload${n}_`;
@@ -549,15 +547,6 @@ function renderPayload(s, total, uciconfig) {
o.depends('type', key); o.depends('type', key);
}) })
initPayload(o, n, 'type'); initPayload(o, n, 'type');
o.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'entry');
let n = this.option.match(/^payload(\d+)_/)[1];
let rule = new RulesEntry(UIEl.getValue()).setPayload(n, {type: value});
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
}
o = s.option(form.Value, prefix + 'general', _('Factor') + ` ${n+1}`); o = s.option(form.Value, prefix + 'general', _('Factor') + ` ${n+1}`);
if (n === 0) { if (n === 0) {
@@ -608,48 +597,40 @@ function renderPayload(s, total, uciconfig) {
if (n === 0) if (n === 0)
o.depends('type', 'IN-TYPE'); o.depends('type', 'IN-TYPE');
o.depends(prefix + 'type', 'IN-TYPE'); o.depends(prefix + 'type', 'IN-TYPE');
initPayload(o, n, 'factor'); initPayload(o, n, 'factor',
o.load = L.bind(function(n, key, uciconfig, section_id) { (n, k, inst) => (inst.getPayload(n)[k] || '').split('/'),
return (new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key] || '') (n, k, v, inst) => inst.setPayload(n, Object.fromEntries([[k, v.join('/')]]))
.split('/'); );
}, o, n, 'factor', uciconfig)
o.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'entry');
let n = this.option.match(/^payload(\d+)_/)[1];
let rule = new RulesEntry(UIEl.getValue()).setPayload(n, {factor: value.join('/')});
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
}
o = s.option(form.ListValue, prefix + 'inname', _('Factor') + ` ${n+1}`); o = s.option(form.ListValue, prefix + 'inname', _('Factor') + ` ${n+1}`);
if (n === 0) if (n === 0)
o.depends('type', 'IN-NAME'); o.depends('type', 'IN-NAME');
o.depends(prefix + 'type', 'IN-NAME'); o.depends(prefix + 'type', 'IN-NAME');
initPayload(o, n, 'factor'); initPayload(o, n, 'factor');
o.load = L.bind(function(n, key, uciconfig, section_id) { const inname_old_load = o.load;
o.load = function(section_id) {
hm.loadLabel.call(this, [ hm.loadLabel.call(this, [
['', _('-- Please choose --')], ['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'inbound') ...hm.loadLabelValues(this.config, 'inbound')
], section_id); ], section_id);
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key]; return inname_old_load.apply(this, arguments);
}, o, n, 'factor', uciconfig) }
o = s.option(form.ListValue, prefix + 'rematchname', _('Factor') + ` ${n+1}`); o = s.option(form.ListValue, prefix + 'rematchname', _('Factor') + ` ${n+1}`);
if (n === 0) if (n === 0)
o.depends('type', 'REMATCH-NAME'); o.depends('type', 'REMATCH-NAME');
o.depends(prefix + 'type', 'REMATCH-NAME'); o.depends(prefix + 'type', 'REMATCH-NAME');
initPayload(o, n, 'factor'); initPayload(o, n, 'factor');
o.load = L.bind(function(n, key, uciconfig, section_id) { const rematchname_old_load = o.load;
o.load = function(section_id) {
hm.loadLabel.call(this, [ hm.loadLabel.call(this, [
['', _('-- Please choose --')], ['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'rematch-name') ...hm.loadLabelValues(this.config, 'rematch-name')
], section_id); ], section_id);
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key]; return rematchname_old_load.apply(this, arguments);
}, o, n, 'factor', uciconfig) }
o = s.option(form.Value, prefix + 'uint', _('Factor') + ` ${n+1}`); o = s.option(form.Value, prefix + 'uint', _('Factor') + ` ${n+1}`);
o.datatype = 'uinteger'; o.datatype = 'uinteger';
@@ -678,28 +659,20 @@ function renderPayload(s, total, uciconfig) {
o.depends('type', 'RULE-SET'); o.depends('type', 'RULE-SET');
o.depends(prefix + 'type', 'RULE-SET'); o.depends(prefix + 'type', 'RULE-SET');
initPayload(o, n, 'factor'); initPayload(o, n, 'factor');
o.load = L.bind(function(n, key, uciconfig, section_id) { const rule_set_old_load = o.load;
o.load = function(section_id) {
hm.loadLabel.call(this, hm.loadLabelValues(this.config, 'ruleset'), section_id); hm.loadLabel.call(this, hm.loadLabelValues(this.config, 'ruleset'), section_id);
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key]; return rule_set_old_load.apply(this, arguments);
}, o, n, 'factor', uciconfig) }
o = s.option(form.Flag, prefix + 'NOT', _('NOT') + ` ${n+1}`); o = s.option(form.Flag, prefix + 'NOT', _('NOT') + ` ${n+1}`);
o.default = o.disabled; o.default = o.disabled;
o.depends(Object.fromEntries([[prefix + 'type', /.+/]])); o.depends(Object.fromEntries([[prefix + 'type', /.+/]]));
initPayload(o, n, 'deny'); initPayload(o, n, 'deny',
o.load = L.bind(function(n, key, uciconfig, section_id) { (n, k, inst) => boolToFlag(inst.getPayload(n)[k] ? true : false),
return boolToFlag(new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key] ? true : false); (n, k, v, inst) => inst.setPayload(n, Object.fromEntries([[k, flagToBool(v) || null]]))
}, o, n, 'deny', uciconfig); );
o.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'entry');
let n = this.option.match(/^payload(\d+)_/)[1];
let rule = new RulesEntry(UIEl.getValue()).setPayload(n, {deny: flagToBool(value) || null});
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
}
} }
// DynamicList payload // DynamicList payload
@@ -721,23 +694,15 @@ function renderPayload(s, total, uciconfig) {
extenbox[n].forEach((type) => { extenbox[n].forEach((type) => {
o.depends('type', type); o.depends('type', type);
}) })
initDynamicPayload(o, n, 'type'); initDynamicPayload(o, n, 'type',
o.validate = function(section_id, value) { undefined,
value = this.formvalue(section_id); (n, k, v, inst) => {
let UIEl = this.section.getUIElement(section_id, 'entry'); v.forEach((val) => {
let rule = new RulesEntry(UIEl.getValue()); inst.setPayload(n, Object.fromEntries([[k, val]])); n++;
});
let n = this.option.match(/^payload(\d+)_/)[1]; inst.setPayload(n, Object.fromEntries([[k, null]]), n);
value.forEach((val) => { }
rule.setPayload(n, {type: val}); n++; );
});
rule.setPayload(n, {type: null}, n);
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
return true;
}
o = s.option(hm.GroupDynamicList, prefix + 'fused', _('Factor') + ' ++', o = s.option(hm.GroupDynamicList, prefix + 'fused', _('Factor') + ' ++',
_('Content will not be verified, Please make sure you enter it correctly.')); _('Content will not be verified, Please make sure you enter it correctly.'));
@@ -745,7 +710,8 @@ function renderPayload(s, total, uciconfig) {
o.depends(Object.fromEntries([['type', type], [prefix + 'type', /.+/]])); o.depends(Object.fromEntries([['type', type], [prefix + 'type', /.+/]]));
}) })
initDynamicPayload(o, n, 'factor'); initDynamicPayload(o, n, 'factor');
o.load = L.bind(function(n, key, uciconfig, section_id) { const fused_old_load = o.load;
o.load = function(section_id) {
hm.loadLabel.call(this, [ hm.loadLabel.call(this, [
...hm.inbound_type.map(e => [e, _('IN-TYPE')]), // Multi-values are not supported ...hm.inbound_type.map(e => [e, _('IN-TYPE')]), // Multi-values are not supported
...hm.loadLabelValues(this.config, 'inbound').map(e => [e, _('IN-NAME')]), ...hm.loadLabelValues(this.config, 'inbound').map(e => [e, _('IN-NAME')]),
@@ -755,8 +721,8 @@ function renderPayload(s, total, uciconfig) {
...hm.loadLabelValues(this.config, 'ruleset').map(e => [e, _('RULE-SET')]) ...hm.loadLabelValues(this.config, 'ruleset').map(e => [e, _('RULE-SET')])
], section_id); ], section_id);
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayloads().slice(n).map(e => e[key] ?? ''); return fused_old_load.apply(this, arguments);
}, o, n, 'factor', uciconfig) }
o = s.option(hm.StaticList, prefix + 'NOTs', _('NOT') + ' ++', o = s.option(hm.StaticList, prefix + 'NOTs', _('NOT') + ' ++',
_('<code>0</code> or <code>1</code> only.')); _('<code>0</code> or <code>1</code> only.'));
@@ -765,27 +731,16 @@ function renderPayload(s, total, uciconfig) {
extenbox[n].forEach((type) => { extenbox[n].forEach((type) => {
o.depends(Object.fromEntries([['type', type], [prefix + 'type', /.+/]])); o.depends(Object.fromEntries([['type', type], [prefix + 'type', /.+/]]));
}) })
initDynamicPayload(o, n, 'deny'); initDynamicPayload(o, n, 'deny',
o.load = L.bind(function(n, key, uciconfig, section_id) { (n, k, inst) => inst.getPayloads().slice(n).map(e => boolToFlag(e[k] ? true : false)),
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayloads().slice(n).map(e => boolToFlag(e[key] ? true : false)); (n, k, v, inst) => {
}, o, n, 'deny', uciconfig); const limit = inst.getPayloads().length;
o.validate = function(section_id, value) { v.forEach((val) => {
value = this.formvalue(section_id); inst.setPayload(n, Object.fromEntries([[k, flagToBool(val) || null]])); n++;
let UIEl = this.section.getUIElement(section_id, 'entry'); });
let rule = new RulesEntry(UIEl.getValue()); inst.setPayload(limit, Object.fromEntries([[k, null]]), limit);
}
let n = this.option.match(/^payload(\d+)_/)[1]; );
let limit = rule.getPayloads().length;
value.forEach((value) => {
rule.setPayload(n, {deny: flagToBool(value) || null}); n++;
});
rule.setPayload(limit, {deny: null}, limit);
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
return true;
}
}) })
} }
@@ -801,94 +756,90 @@ function renderRules(s, uciconfig) {
return El; return El;
} }
o.load = function(section_id) { o.load = function(section_id) {
return form.DummyValue.prototype.load.call(this, section_id) || new RulesEntry().toString('json'); const value = this.super('load', section_id);
this[`${this.option}_${section_id}`] = new RulesEntry(value);
return value;
}
o.instget = function(section_id) {return this[`${this.option}_${section_id}`]}
o.instset = function(section_id, callback = inst => inst) {
const UIEl = this.getUIElement(section_id);
const inst = this[`${this.option}_${section_id}`];
callback(inst);
UIEl.node.previousSibling.innerText = inst.toString('mihomo');
UIEl.setValue(inst.toString('json'));
} }
o.write = form.AbstractValue.prototype.write; o.write = form.AbstractValue.prototype.write;
o.remove = form.AbstractValue.prototype.remove; o.remove = form.AbstractValue.prototype.remove;
o.editable = true; o.editable = true;
// common part
const initEntryOption = function(o, key,
load_callback = (k, inst) => inst[k],
onchange_callback = (k, v, inst) => inst.setKey(k, v)
) {
o.load = function(section_id) {
return load_callback(key, this.section.getOption('entry').instget(section_id));
}
o.onchange = function(ev, section_id, value) {
this.section.getOption('entry').instset(section_id, L.bind(onchange_callback, this, key, value));
}
o.write = function() {};
}
o = s.option(hm.GroupListValue, 'type', _('Type')); o = s.option(hm.GroupListValue, 'type', _('Type'));
o.default = hm.rules_type[0].types[0][0]; o.default = hm.rules_type[0].types[0][0];
for (const {label, types} of hm.rules_type.concat(hm.rules_logical_type)) { for (const {label, types} of hm.rules_type.concat(hm.rules_logical_type)) {
types.forEach((res) => o.value.call(o, res, label)); types.forEach((res) => o.value.call(o, res, label));
} }
o.load = function(section_id) { initEntryOption(o, 'type');
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).type;
}
o.validate = function(section_id, value) { o.validate = function(section_id, value) {
// params only available for types other than // params only available for types other than
if (hm.rules_type_allowparms.includes(value)) { const params = ['no-resolve', 'src'];
['no-resolve', 'src'].forEach((opt) => { for (let param of params) {
let UIEl = this.section.getUIElement(section_id, opt); let UIEl = this.section.getUIElement(section_id, param);
if (hm.rules_type_allowparms.includes(value))
UIEl.node.querySelector('input').removeAttribute('disabled'); UIEl.node.querySelector('input').removeAttribute('disabled');
}); else {
} else {
['no-resolve', 'src'].forEach((opt) => {
let UIEl = this.section.getUIElement(section_id, opt);
UIEl.setValue(''); UIEl.setValue('');
UIEl.node.querySelector('input').disabled = true; UIEl.node.querySelector('input').disabled = true;
});
let UIEl = this.section.getUIElement(section_id, 'entry'); this.section.getOption('entry').instset(section_id, inst => params.forEach(param => inst.setParam(param)));
}
let rule = new RulesEntry(UIEl.getValue()).setParam('no-resolve').setParam('src');
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
} }
return true; return true;
} }
o.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'entry');
let rule = new RulesEntry(UIEl.getValue()).setKey('type', value);
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
}
o.write = function() {};
o.rmempty = false; o.rmempty = false;
o.modalonly = true; o.modalonly = true;
renderPayload(s, Math.max(...Object.values(hm.rules_logical_payload_count).map(e => e.low)), uciconfig); renderPayload(s, Math.max(...Object.values(hm.rules_logical_payload_count).map(e => e.low)), uciconfig);
o = s.option(hm.ListValue, 'detour', _('Proxy group')); o = s.option(hm.ListValue, 'detour', _('Proxy group'));
initEntryOption(o, 'detour');
const detour_old_load = o.load;
o.load = function(section_id) { o.load = function(section_id) {
hm.loadLabel.call(this, [ hm.loadLabel.call(this, [
...hm.preset_outbound.full, ...hm.preset_outbound.full,
...hm.loadLabelValues(this.config, 'proxy_group') ...hm.loadLabelValues(this.config, 'proxy_group')
], section_id); ], section_id);
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).detour; return detour_old_load.apply(this, arguments);
} }
o.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'entry');
let rule = new RulesEntry(UIEl.getValue()).setKey('detour', value);
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
}
o.write = function() {};
//o.depends('SUB-RULE', ''); // work on subrules not rules //o.depends('SUB-RULE', ''); // work on subrules not rules
o.editable = true; o.editable = true;
o = s.option(form.Flag, 'src', _('src'), o = s.option(form.Flag, 'src', _('src'),
_('Treat the <code>destination IP</code> as the <code>source IP</code>.')); _('Treat the <code>destination IP</code> as the <code>source IP</code>.'));
o.default = o.disabled; o.default = o.disabled;
o.load = function(section_id) { initEntryOption(o, 'src',
return boolToFlag(new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getParam('src') ? true : false); (k, inst) => boolToFlag(inst.getParam(k) ? true : false),
} (k, v, inst) => inst.setParam(k, flagToBool(v) || null)
o.onchange = function(ev, section_id, value) { );
let UIEl = this.section.getUIElement(section_id, 'entry');
let rule = new RulesEntry(UIEl.getValue()).setParam('src', flagToBool(value) || null);
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
}
o.write = function() {};
o.depends('SUB-RULE', ''); o.depends('SUB-RULE', '');
o.modalonly = true; o.modalonly = true;
@@ -896,18 +847,10 @@ function renderRules(s, uciconfig) {
_('Do not resolve the domain connection to IP for this match.</br>' + _('Do not resolve the domain connection to IP for this match.</br>' +
'Only works for pure domain inbound connections without DNS resolution. e.g., socks5h')); 'Only works for pure domain inbound connections without DNS resolution. e.g., socks5h'));
o.default = o.disabled; o.default = o.disabled;
o.load = function(section_id) { initEntryOption(o, 'no-resolve',
return boolToFlag(new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getParam('no-resolve') ? true : false); (k, inst) => boolToFlag(inst.getParam(k) ? true : false),
} (k, v, inst) => inst.setParam(k, flagToBool(v) || null)
o.onchange = function(ev, section_id, value) { );
let UIEl = this.section.getUIElement(section_id, 'entry');
let rule = new RulesEntry(UIEl.getValue()).setParam('no-resolve', flagToBool(value) || null);
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
}
o.write = function() {};
o.depends('SUB-RULE', ''); o.depends('SUB-RULE', '');
o.modalonly = true; o.modalonly = true;
} }
@@ -1421,7 +1364,10 @@ return view.extend({
...hm.loadLabelValues(this.config, 'subrule-group') ...hm.loadLabelValues(this.config, 'subrule-group')
], section_id); ], section_id);
return new RulesEntry(uci.get(data[0], section_id, 'entry')).subrule || ''; return this.section.getOption('entry').instget(section_id).subrule || '';
}
so.onchange = function(ev, section_id, value) {
this.section.getOption('entry').instset(section_id, inst => inst.setKey('subrule', value));
} }
so.validate = function(section_id, value) { so.validate = function(section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'detour'); let UIEl = this.section.getUIElement(section_id, 'detour');
@@ -1431,14 +1377,6 @@ return view.extend({
return true; return true;
} }
so.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'entry');
let rule = new RulesEntry(UIEl.getValue()).setKey('subrule', value);
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
UIEl.setValue(rule.toString('json'));
}
so.write = function() {}; so.write = function() {};
so.modalonly = true; so.modalonly = true;
/* Routing rules END */ /* Routing rules END */
@@ -1642,85 +1580,84 @@ return view.extend({
so.editable = true; so.editable = true;
so = ss.option(form.DummyValue, 'address', _('Address')); so = ss.option(form.DummyValue, 'address', _('Address'));
so.load = function(section_id) {
const value = this.super('load', section_id);
this[`${this.option}_${section_id}`] = new DNSAddress(value);
return value;
}
so.instget = function(section_id) {return this[`${this.option}_${section_id}`]}
so.instset = function(section_id, callback = inst => inst) {
const UIEl = this.getUIElement(section_id);
const inst = this[`${this.option}_${section_id}`];
callback(inst);
UIEl.node.previousSibling.innerText = inst.toString();
UIEl.setValue(inst.toString());
}
so.write = form.AbstractValue.prototype.write; so.write = form.AbstractValue.prototype.write;
so.remove = form.AbstractValue.prototype.remove; so.remove = form.AbstractValue.prototype.remove;
so.editable = true; so.editable = true;
so = ss.option(form.Value, 'addr', _('Address')); // common part
so.load = function(section_id) { const initAddressOption = function(o, key,
return new DNSAddress(uci.get(data[0], section_id, 'address')).addr; load_callback = (k, inst) => boolToFlag(inst.parseParam(k) ? true : false),
onchange_callback = (k, v, inst) => inst.setParam(k, flagToBool(v) || null)
) {
o.load = function(section_id) {
return load_callback(key, this.section.getOption('address').instget(section_id));
}
o.onchange = function(ev, section_id, value) {
this.section.getOption('address').instset(section_id, L.bind(onchange_callback, this, key, value));
}
o.write = function() {};
} }
so = ss.option(form.Value, 'addr', _('Address'));
initAddressOption(so, 'addr',
(k, inst) => inst[k],
(k, v, inst) => inst[k] = v
);
so.validate = function(section_id, value) { so.validate = function(section_id, value) {
if (value.match('#')) if (value.match('#'))
return _('Expecting: %s').format(_('No add\'l params')); return _('Expecting: %s').format(_('No add\'l params'));
// params only available on DoH let UIEl = this.section.getUIElement(section_id, 'h3');
// https://github.com/muink/mihomo/blob/43f21c0b412b7a8701fe7a2ea6510c5b985a53d6/config/config.go#L1211C8-L1211C14 if (value.match(/^https?:\/\//))
if (value.match(/^https?:\/\//)){ UIEl.node.querySelector('input').removeAttribute('disabled');
this.section.getUIElement(section_id, 'h3').node.querySelector('input').removeAttribute('disabled'); else {
} else { this.section.getOption('address').instset(section_id, inst => inst.setParam('h3'));
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('h3').toString(); UIEl.setValue('');
UIEl.node.querySelector('input').disabled = true;
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
['h3'].forEach((opt) => {
let UIEl = this.section.getUIElement(section_id, opt);
UIEl.setValue('');
UIEl.node.querySelector('input').disabled = true;
});
} }
return true; return true;
} }
so.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = ('N' + UIEl.getValue()).replace(/^[^#]+/, value);
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
}
so.write = function() {};
so.rmempty = false; so.rmempty = false;
so.modalonly = true; so.modalonly = true;
so = ss.option(hm.ListValue, 'detour', _('Proxy group')); so = ss.option(hm.ListValue, 'detour', _('Proxy group'));
initAddressOption(so, 'detour',
(k, inst) => inst.parseParam(k),
(k, v, inst) => inst.setParam(k, v)
);
const detour_old_load = so.load;
so.load = function(section_id) { so.load = function(section_id) {
hm.loadLabel.call(this, [ hm.loadLabel.call(this, [
...hm.preset_outbound.dns, ...hm.preset_outbound.dns,
...hm.loadLabelValues(this.config, 'proxy_group') ...hm.loadLabelValues(this.config, 'proxy_group')
], section_id); ], section_id);
return new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('detour'); return detour_old_load.apply(this, arguments);
} }
so.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('detour', value).toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
}
so.write = function() {};
so.editable = true; so.editable = true;
so = ss.option(form.Flag, 'h3', _('HTTP/3')); so = ss.option(form.Flag, 'h3', _('HTTP/3'));
so.default = so.disabled; so.default = so.disabled;
so.load = function(section_id) { initAddressOption(so, 'h3');
return boolToFlag(new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('h3') ? true : false);
}
so.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('h3', flagToBool(value) || null).toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
}
so.write = function() {};
so.modalonly = true; so.modalonly = true;
so = ss.option(form.Flag, 'skip-cert-verify', _('Skip cert verify'), so = ss.option(form.Flag, 'skip-cert-verify', _('Skip cert verify'),
@@ -1728,101 +1665,42 @@ return view.extend({
'<br/>' + '<br/>' +
_('This is <strong>DANGEROUS</strong>, your traffic is almost like <strong>PLAIN TEXT</strong>! Use at your own risk!')); _('This is <strong>DANGEROUS</strong>, your traffic is almost like <strong>PLAIN TEXT</strong>! Use at your own risk!'));
so.default = so.disabled; so.default = so.disabled;
so.load = function(section_id) { initAddressOption(so, 'skip-cert-verify');
return boolToFlag(new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('skip-cert-verify') ? true : false);
}
so.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('skip-cert-verify', flagToBool(value) || null).toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
}
so.write = function() {};
so.modalonly = true; so.modalonly = true;
so = ss.option(form.Value, 'ecs', _('EDNS Client Subnet')); so = ss.option(form.Value, 'ecs', _('EDNS Client Subnet'));
so.datatype = 'cidr'; so.datatype = 'cidr';
so.load = function(section_id) { initAddressOption(so, 'ecs',
return new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('ecs'); (k, inst) => inst.parseParam(k),
} (k, v, inst) => inst.setParam(k, v)
so.onchange = function(ev, section_id, value) { );
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('ecs', value).toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
}
so.write = function() {};
so.modalonly = true; so.modalonly = true;
so = ss.option(form.Flag, 'ecs-override', _('ECS override'), so = ss.option(form.Flag, 'ecs-override', _('ECS override'),
_('Override the existing ECS in original request.')); _('Override the existing ECS in original request.'));
so.default = so.disabled; so.default = so.disabled;
so.load = function(section_id) { initAddressOption(so, 'ecs-override');
return boolToFlag(new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('ecs-override') ? true : false);
}
so.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('ecs-override', flagToBool(value) || null).toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
}
so.write = function() {};
so.depends({'ecs': /.+/}); so.depends({'ecs': /.+/});
so.modalonly = true; so.modalonly = true;
so = ss.option(form.Flag, 'disable-ipv4', _('Filter record: %s').format('A')); so = ss.option(form.Flag, 'disable-ipv4', _('Filter record: %s').format('A'));
so.default = so.disabled; so.default = so.disabled;
so.load = function(section_id) { initAddressOption(so, 'disable-ipv4');
return boolToFlag(new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('disable-ipv4') ? true : false);
}
so.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('disable-ipv4', flagToBool(value) || null).toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
}
so.write = function() {}; so.write = function() {};
so.modalonly = true; so.modalonly = true;
so = ss.option(form.Flag, 'disable-ipv6', _('Filter record: %s').format('AAAA')); so = ss.option(form.Flag, 'disable-ipv6', _('Filter record: %s').format('AAAA'));
so.default = so.disabled; so.default = so.disabled;
so.load = function(section_id) { initAddressOption(so, 'disable-ipv6');
return boolToFlag(new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('disable-ipv6') ? true : false);
}
so.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('disable-ipv6', flagToBool(value) || null).toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
}
so.write = function() {};
so.modalonly = true; so.modalonly = true;
so = ss.option(form.DynamicList, 'disable-qtypes', _('Filter record type:')); so = ss.option(form.DynamicList, 'disable-qtypes', _('Filter record type:'));
so.datatype = 'uinteger'; so.datatype = 'uinteger';
so.placeholder = '65'; so.placeholder = '65';
so.load = function(section_id) { initAddressOption(so, 'disable-qtypes',
return new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('disable-qtypes'); (k, inst) => inst.parseParam(k),
} (k, v, inst) => inst.setParam(k, v)
so.onchange = function(ev, section_id, value) { );
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('disable-qtypes', value).toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
}
so.write = function() {};
so.modalonly = true; so.modalonly = true;
/* DNS server END */ /* DNS server END */
+2 -2
View File
@@ -2,10 +2,10 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-flowproxy PKG_NAME:=luci-app-flowproxy
PKG_VERSION:=1.0.3 PKG_VERSION:=1.0.3
PKG_RELEASE:=6 PKG_RELEASE:=7
LUCI_TITLE:=LuCI Support for FlowProxy Traffic Diversion LUCI_TITLE:=LuCI Support for FlowProxy Traffic Diversion
LUCI_DEPENDS:=+luci-base +nftables +ip-full LUCI_DEPENDS:=+luci-base +nftables +ip-full +iputils-arping
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
define Package/$(PKG_NAME)/conffiles define Package/$(PKG_NAME)/conffiles
@@ -75,12 +75,21 @@ return L.view.extend({
return callGetStatus().then(L.bind(function(status) { return callGetStatus().then(L.bind(function(status) {
if (!status) return; if (!status) return;
var isRunning = (status.running == 1); var isRunning = (status.running == 1);
var isDegraded = (status.degraded == 1);
var container = node || document; var container = node || document;
var statusEl = container.querySelector('#service-status'); var statusEl = container.querySelector('#service-status');
if (statusEl) { if (statusEl) {
var content = []; var content = [];
if (isRunning) { if (isRunning && isDegraded) {
content.push(E('span', { 'style': 'color: #FF9800; font-weight: bold;' }, [ _('degraded (direct fallback)') ]));
content.push(' ');
content.push(E('button', {
'class': 'btn cbi-button cbi-button-apply',
'style': 'margin-left: 10px;',
'click': ui.createHandlerFn(this, 'handleRestart')
}, [ _('Restart') ]));
} else if (isRunning) {
var chains = []; var chains = [];
if (status.nft && status.nft.tcp) chains.push('TCP'); if (status.nft && status.nft.tcp) chains.push('TCP');
if (status.nft && status.nft.udp) chains.push('UDP'); if (status.nft && status.nft.udp) chains.push('UDP');
@@ -187,6 +196,9 @@ return L.view.extend({
s.taboption('settings', form.Flag, 'enabled', _('Enable FlowProxy')).rmempty = false; s.taboption('settings', form.Flag, 'enabled', _('Enable FlowProxy')).rmempty = false;
s.taboption('settings', form.Flag, 'dns_proxy_enabled', _('Force upstream DNS to proxy server')).rmempty = false; s.taboption('settings', form.Flag, 'dns_proxy_enabled', _('Force upstream DNS to proxy server')).rmempty = false;
o = s.taboption('settings', form.Flag, 'monitor_enabled', _('Enable proxy server monitoring'));
o.default = '0';
o.rmempty = false;
o = s.taboption('settings', form.Value, 'proxy_server_ip_addr', _('Proxy server IP address')); o = s.taboption('settings', form.Value, 'proxy_server_ip_addr', _('Proxy server IP address'));
o.datatype = 'ip4addr'; o.rmempty = true; o.datatype = 'ip4addr'; o.rmempty = true;
@@ -226,3 +226,9 @@ msgstr ""
msgid "this will delete ALL current %s rules and generate default templates. are you sure?" msgid "this will delete ALL current %s rules and generate default templates. are you sure?"
msgstr "" msgstr ""
msgid "Enable proxy server monitoring"
msgstr ""
msgid "degraded (direct fallback)"
msgstr ""
@@ -250,3 +250,9 @@ msgstr "RPC 错误: %s"
msgid "The proxy server IP address must be within the subnet of the selected interface (%s)." msgid "The proxy server IP address must be within the subnet of the selected interface (%s)."
msgstr "代理服务器 IP 地址必须在所选网络接口的子网网段之内 (%s)。" msgstr "代理服务器 IP 地址必须在所选网络接口的子网网段之内 (%s)。"
msgid "Enable proxy server monitoring"
msgstr "启用代理服务器监控"
msgid "degraded (direct fallback)"
msgstr "已降级 (回退直连状态)"
@@ -8,6 +8,7 @@ config flowproxy 'global'
option routing_table '888' option routing_table '888'
option rule_priority '1000' option rule_priority '1000'
option dns_proxy_enabled '0' option dns_proxy_enabled '0'
option monitor_enabled '0'
option proxy_server_dns_port '5353' option proxy_server_dns_port '5353'
option log_level 'info' option log_level 'info'
option log_size '1024' option log_size '1024'
+58 -271
View File
@@ -5,299 +5,86 @@ START=99
STOP=10 STOP=10
USE_PROCD=1 USE_PROCD=1
NAME="flowproxy" . /usr/share/flowproxy/common.sh
UCI_CONFIG="flowproxy"
LOG_FILE="/var/log/flowproxy.log"
NFT_CONF="/tmp/flowproxy/nft.conf"
GENERATE_SCRIPT="/usr/share/flowproxy/generate_nft.sh"
STATE_FILE="/var/run/flowproxy.running"
get_level_weight() { _stop_unlocked() {
case "$1" in log_msg info "Stopping FlowProxy..."
debug) echo 0 ;; # 强制先移除标记,防止状态误报及旧守护进程继续动作
info) echo 1 ;; rm -f "$STATE_FILE" "$DEGRADED_FILE" "$GEN_FILE"
warn) echo 2 ;; delete_route
error) echo 3 ;; delete_nft
*) echo 1 ;; cleanup_dns
esac rm -f "$NFT_CONF"
log_msg info "FlowProxy service stopped"
} }
log_msg() { _start_unlocked() {
local level="$1"; shift local enabled=$(uci -q get "$UCI_CONFIG.global.enabled")
local msg="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# 等级过滤 if [ "${enabled:-0}" != "1" ]; then
local conf_level=$(uci -q get "$UCI_CONFIG.global.log_level" || echo "info") log_msg info "FlowProxy is disabled, ensuring service is stopped"
local conf_weight=$(get_level_weight "$conf_level") _stop_unlocked
local msg_weight=$(get_level_weight "$level") return 0
[ "$msg_weight" -lt "$conf_weight" ] && return fi
# 简单的日志截断逻辑,防止日志无限增长 log_msg info "Starting FlowProxy..."
# 读取配置的大小限制,默认 1024KB
local max_size=$(uci -q get "$UCI_CONFIG.global.log_size" || echo 1024)
local current_size=$(du -k "$LOG_FILE" 2>/dev/null | cut -f1)
if [ "${current_size:-0}" -gt "$max_size" ]; then
mv "$LOG_FILE" "$LOG_FILE.old"
touch "$LOG_FILE"
fi
echo "[$timestamp] [$level] $msg" >> "$LOG_FILE" check_env || return 1
} check_config || return 1
apply_nft_rules || return 1
add_route || { delete_nft; return 1; }
setup_dns
check_env() { rm -f "$DEGRADED_FILE"
# 检测核心二进制工具是否存在 touch "$STATE_FILE"
if ! command -v nft >/dev/null 2>&1; then
log_msg error "nftables binary not found"
return 1
fi
return 0
}
check_config() { local gen="$(date +%s)-$$"
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr") echo "$gen" > "$GEN_FILE"
[ -z "$proxy_server_ip_addr" ] && { log_msg error "proxy_server_ip_addr not configured"; return 1; }
return 0
}
apply_nft_rules() { local monitor_enabled=$(uci -q get "$UCI_CONFIG.global.monitor_enabled" || echo 0)
log_msg info "Generating nftables configuration..." if [ "$monitor_enabled" = "1" ]; then
local conf_level=$(uci -q get "$UCI_CONFIG.global.log_level" || echo "info") procd_open_instance "watchdog"
procd_set_param command /usr/share/flowproxy/watchdog.sh "$gen"
procd_set_param respawn 3600 5 5
procd_close_instance
fi
mkdir -p "$(dirname "$NFT_CONF")" log_msg info "FlowProxy service started successfully"
if [ -x "$GENERATE_SCRIPT" ]; then return 0
# 传递日志等级并捕获 stderr 到临时文件
local err_file="/tmp/flowproxy/gen_err.tmp"
FLOWPROXY_LOG_LEVEL="$conf_level" "$GENERATE_SCRIPT" >/dev/null 2> "$err_file"
local ret=$?
# 将脚本的输出记录到日志
if [ -s "$err_file" ]; then
while read -r line; do
log_msg debug "Script: $line"
done < "$err_file"
rm -f "$err_file"
fi
if [ $ret -eq 0 ]; then
log_msg info "Applying nftables rules from $NFT_CONF"
# 确保内核支持 inet 表
nft delete table inet flowproxy 2>/dev/null
if nft -f "$NFT_CONF"; then
return 0
else
log_msg error "nftables rules application failed (syntax error)"
return 1
fi
else
log_msg error "Failed to generate nftables config"
return 1
fi
else
log_msg error "Generator script $GENERATE_SCRIPT not found or not executable"
return 1
fi
}
add_route() {
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
local interface=$(uci -q get "$UCI_CONFIG.global.interface" || echo "br-lan")
local traffic_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark" || echo "0x666")
local routing_table=$(uci -q get "$UCI_CONFIG.global.routing_table" || echo "888")
local rule_priority=$(uci -q get "$UCI_CONFIG.global.rule_priority" || echo "1000")
# 1. 先尝试用旧记录清理一次,防止由于配置变更导致的残留
delete_route "quiet"
log_msg info "Setting up routing table $routing_table and rule for mark $traffic_mark with priority $rule_priority..."
# 2. 记录当前使用的标记值、路由表 ID 和优先级
echo "$traffic_mark:$routing_table:$rule_priority" > "/var/run/flowproxy.mark"
# 3. 再次确保新标记和表的规则是干净的
while ip rule del fwmark "$traffic_mark" lookup "$routing_table" 2>/dev/null; do :; done
ip route flush table "$routing_table" 2>/dev/null
# 4. 添加路由和策略
local err
ip route add default via "$proxy_server_ip_addr" dev "$interface" table "$routing_table" 2>/dev/null
err=$(ip rule add fwmark "$traffic_mark" lookup "$routing_table" priority "$rule_priority" 2>&1)
if [ $? -eq 0 ]; then
log_msg info "Route added: via $proxy_server_ip_addr dev $interface table $routing_table for mark $traffic_mark with priority $rule_priority"
else
log_msg error "Failed to add ip rule: $err"
fi
}
delete_route() {
local traffic_mark routing_table rule_priority
local quiet="$1"
# 1. 获取两组清理目标:旧值(记录文件) 和 新值(当前UCI)
local old_mark="" old_table="" old_prio=""
if [ -f "/var/run/flowproxy.mark" ]; then
local saved_val=$(cat "/var/run/flowproxy.mark")
old_mark=$(echo "$saved_val" | cut -d':' -f1)
old_table=$(echo "$saved_val" | cut -d':' -f2)
old_prio=$(echo "$saved_val" | cut -d':' -f3)
[ "$quiet" != "quiet" ] && rm -f "/var/run/flowproxy.mark"
fi
local curr_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark" || echo "0x666")
local curr_table=$(uci -q get "$UCI_CONFIG.global.routing_table" || echo "888")
local curr_prio=$(uci -q get "$UCI_CONFIG.global.rule_priority" || echo "1000")
[ "$quiet" != "quiet" ] && log_msg info "Cleaning up FlowProxy routes (Atomic Mode)..."
# 2. 核心清理函数:只清理带 fwmark 且指向特定 table 的规则
safe_del_rule() {
local m="$1" t="$2" p="$3"
[ -z "$m" ] || [ -z "$t" ] && return
log_msg debug "Attempting cleanup: mark=$m, table=$t, priority=${p:-any}"
# 精准清理 (带优先级)
[ -n "$p" ] && while ip rule del fwmark "$m" lookup "$t" priority "$p" 2>/dev/null; do
log_msg debug "Removed stale ip rule: mark $m lookup $t priority $p"
done
# 模糊清理 (不带优先级,防止配置中途被手动改动)
while ip rule del fwmark "$m" lookup "$t" 2>/dev/null; do
log_msg debug "Removed stale ip rule (fuzzy): mark $m lookup $t"
done
# 清空路由表
ip route flush table "$t" 2>/dev/null
log_msg debug "Flushed routing table $t"
}
# 执行清理:先清旧值,再清当前值 (防止残留)
safe_del_rule "$old_mark" "$old_table" "$old_prio"
[ "$old_mark" != "$curr_mark" ] || [ "$old_table" != "$curr_table" ] && \
safe_del_rule "$curr_mark" "$curr_table" "$curr_prio"
}
delete_nft() {
nft delete table inet flowproxy 2>/dev/null
log_msg info "Removed nftables table 'flowproxy'"
}
setup_dns() {
local enabled=$(uci -q get "$UCI_CONFIG.global.enabled" || echo 0)
local dns_proxy_enabled=$(uci -q get "$UCI_CONFIG.global.dns_proxy_enabled" || echo 0)
local proxy_server_dns_port=$(uci -q get "$UCI_CONFIG.global.proxy_server_dns_port" || echo 5353)
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
# Only apply DNS proxy if BOTH main service and DNS proxy are enabled
if [ "$enabled" -eq 0 ] || [ "$dns_proxy_enabled" -eq 0 ]; then
cleanup_dns
return 0
fi
[ -z "$proxy_server_ip_addr" ] && { log_msg error "proxy_server_ip_addr not configured, cannot setup DNS"; return 1; }
log_msg info "Setting up DNS redirection to $proxy_server_ip_addr#$proxy_server_dns_port..."
# Persistent backup in UCI
if ! uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then
local old_noresolv=$(uci -q get dhcp.@dnsmasq[0].noresolv)
local old_dns_redirect=$(uci -q get dhcp.@dnsmasq[0].dns_redirect)
local old_servers=$(uci -q get dhcp.@dnsmasq[0].server | tr '\n' ',' | sed 's/,$//')
log_msg debug "Backing up DNS: noresolv=${old_noresolv:-empty}, redirect=${old_dns_redirect:-empty}, servers=${old_servers:-empty}"
uci set "$UCI_CONFIG.dns_backup=dns_backup"
[ -n "$old_noresolv" ] && uci set "$UCI_CONFIG.dns_backup.noresolv=$old_noresolv"
[ -n "$old_dns_redirect" ] && uci set "$UCI_CONFIG.dns_backup.dns_redirect=$old_dns_redirect"
[ -n "$old_servers" ] && uci set "$UCI_CONFIG.dns_backup.servers=$old_servers"
uci commit "$UCI_CONFIG"
fi
log_msg debug "Applying new DNS: noresolv=1, redirect=0, server=$proxy_server_ip_addr#$proxy_server_dns_port"
uci set dhcp.@dnsmasq[0].noresolv='1'
uci set dhcp.@dnsmasq[0].dns_redirect='0'
# Delete current servers and add our proxy
uci -q delete dhcp.@dnsmasq[0].server
uci add_list dhcp.@dnsmasq[0].server="$proxy_server_ip_addr#$proxy_server_dns_port"
uci commit dhcp
/etc/init.d/dnsmasq restart
}
cleanup_dns() {
if uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then
log_msg info "Restoring original DNS configuration..."
local noresolv=$(uci -q get "$UCI_CONFIG.dns_backup.noresolv")
local dns_redirect=$(uci -q get "$UCI_CONFIG.dns_backup.dns_redirect")
local servers=$(uci -q get "$UCI_CONFIG.dns_backup.servers")
log_msg debug "Restoring DNS values: noresolv=${noresolv:-0}, redirect=${dns_redirect:-1}, servers=${servers:-empty}"
# Restore noresolv (default to 0 if not backed up)
[ -n "$noresolv" ] && uci set dhcp.@dnsmasq[0].noresolv="$noresolv" || uci set dhcp.@dnsmasq[0].noresolv="0"
# Restore dns_redirect (only restore if it originally existed, avoid polluting config)
if [ -n "$dns_redirect" ]; then
uci set dhcp.@dnsmasq[0].dns_redirect="$dns_redirect"
else
uci -q delete dhcp.@dnsmasq[0].dns_redirect
fi
# Restore servers correctly splitting comma-separated list
uci -q delete dhcp.@dnsmasq[0].server
if [ -n "$servers" ]; then
for s in $(echo "$servers" | tr ',' ' '); do
[ -n "$s" ] && uci add_list dhcp.@dnsmasq[0].server="$s"
done
fi
uci commit dhcp
uci delete "$UCI_CONFIG.dns_backup"
uci commit "$UCI_CONFIG"
/etc/init.d/dnsmasq restart
fi
} }
start_service() { start_service() {
local enabled=$(uci -q get "$UCI_CONFIG.global.enabled" || echo 0) acquire_lock
_start_unlocked
if [ "$enabled" -eq 0 ]; then local ret=$?
log_msg info "FlowProxy is disabled, ensuring service is stopped" release_lock
stop_service return $ret
return 0
fi
log_msg info "Starting FlowProxy..."
check_env || return 1
check_config || return 1
apply_nft_rules || return 1
add_route
setup_dns
touch "$STATE_FILE"
log_msg info "FlowProxy service started successfully"
} }
stop_service() { stop_service() {
log_msg info "Stopping FlowProxy..." acquire_lock
# 强制先移除标记,防止状态误报 _stop_unlocked
rm -f "$STATE_FILE" local ret=$?
delete_route release_lock
delete_nft return $ret
cleanup_dns
log_msg info "FlowProxy service stopped"
} }
restart_service() { restart_service() {
stop_service acquire_lock
start_service _stop_unlocked
_start_unlocked
local ret=$?
release_lock
return $ret
} }
reload_service() { reload_service() {
restart_service restart_service
} }
service_triggers() { service_triggers() {
# 监听自身配置改动 # 监听自身配置改动
procd_add_reload_trigger "$UCI_CONFIG" procd_add_reload_trigger "$UCI_CONFIG"
# 监听防火墙重载事件,防止规则被 fw4 清空后无法自动恢复 # 监听防火墙重载事件,防止规则被 fw4 清空后无法自动恢复
procd_add_reload_trigger "firewall" procd_add_reload_trigger "firewall"
} }
+328
View File
@@ -0,0 +1,328 @@
#!/bin/sh
# FlowProxy shared runtime helpers
UCI_CONFIG="flowproxy"
LOG_FILE="/var/log/flowproxy.log"
NFT_CONF="/tmp/flowproxy/nft.conf"
GENERATE_SCRIPT="/usr/share/flowproxy/generate_nft.sh"
STATE_FILE="/var/run/flowproxy.running"
DEGRADED_FILE="/var/run/flowproxy.degraded"
GEN_FILE="/var/run/flowproxy.gen"
MARK_FILE="/var/run/flowproxy.mark"
LOCK_FILE="/var/lock/flowproxy.lock"
get_level_weight() {
case "$1" in
debug) echo 0 ;;
info) echo 1 ;;
warn) echo 2 ;;
error) echo 3 ;;
*) echo 1 ;;
esac
}
log_msg() {
local level="$1"; shift
local msg="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# 等级过滤
local conf_level=$(uci -q get "$UCI_CONFIG.global.log_level")
conf_level="${conf_level:-info}"
local conf_weight=$(get_level_weight "$conf_level")
local msg_weight=$(get_level_weight "$level")
[ "$msg_weight" -lt "$conf_weight" ] && return
# 简单的日志截断逻辑,防止日志无限增长
# 读取配置的大小限制,默认 1024KB
local max_size=$(uci -q get "$UCI_CONFIG.global.log_size")
case "$max_size" in
''|*[!0-9]*) max_size=1024 ;;
esac
local current_size=$(du -k "$LOG_FILE" 2>/dev/null | cut -f1)
if [ "${current_size:-0}" -gt "$max_size" ]; then
mv "$LOG_FILE" "$LOG_FILE.old"
touch "$LOG_FILE"
fi
echo "[$timestamp] [$level] $msg" >> "$LOG_FILE"
}
acquire_lock() {
[ "${FLOWPROXY_LOCKED:-0}" = "1" ] && return 0
mkdir -p "$(dirname "$LOCK_FILE")"
if command -v flock >/dev/null 2>&1; then
exec 9>"$LOCK_FILE"
flock -x 9
else
local wait_cnt=0
while ! mkdir "${LOCK_FILE}.d" 2>/dev/null; do
wait_cnt=$((wait_cnt + 1))
[ "$wait_cnt" -ge 50 ] && break
sleep 1
done
fi
FLOWPROXY_LOCKED=1
return 0
}
release_lock() {
[ "${FLOWPROXY_LOCKED:-0}" != "1" ] && return 0
if command -v flock >/dev/null 2>&1; then
flock -u 9 2>/dev/null
exec 9>&-
else
rmdir "${LOCK_FILE}.d" 2>/dev/null
fi
FLOWPROXY_LOCKED=0
return 0
}
check_env() {
# 检测核心二进制工具是否存在
if ! command -v nft >/dev/null 2>&1; then
log_msg error "nftables binary not found"
return 1
fi
return 0
}
check_config() {
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
[ -z "$proxy_server_ip_addr" ] && { log_msg error "proxy_server_ip_addr not configured"; return 1; }
return 0
}
apply_nft_rules() {
log_msg info "Generating nftables configuration..."
local conf_level=$(uci -q get "$UCI_CONFIG.global.log_level" || echo "info")
mkdir -p "$(dirname "$NFT_CONF")"
if [ -x "$GENERATE_SCRIPT" ]; then
# 传递日志等级并捕获 stderr 到临时文件
local err_file="/tmp/flowproxy/gen_err.tmp"
FLOWPROXY_LOG_LEVEL="$conf_level" "$GENERATE_SCRIPT" >/dev/null 2> "$err_file"
local ret=$?
# 将脚本的输出记录到日志
if [ -s "$err_file" ]; then
while read -r line; do
log_msg debug "Script: $line"
done < "$err_file"
fi
rm -f "$err_file"
if [ $ret -eq 0 ]; then
log_msg info "Applying nftables rules from $NFT_CONF"
# 确保内核支持 inet 表
nft delete table inet flowproxy 2>/dev/null
if nft -f "$NFT_CONF"; then
return 0
else
log_msg error "nftables rules application failed (syntax error)"
return 1
fi
else
log_msg error "Failed to generate nftables config"
return 1
fi
else
log_msg error "Generator script $GENERATE_SCRIPT not found or not executable"
return 1
fi
}
restore_nft_rules() {
if [ -s "$NFT_CONF" ]; then
log_msg info "Restoring nftables rules from cached $NFT_CONF"
nft delete table inet flowproxy 2>/dev/null
if nft -f "$NFT_CONF"; then
return 0
fi
log_msg warn "Cached nftables config failed, regenerating..."
fi
apply_nft_rules
}
add_route() {
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
local interface=$(uci -q get "$UCI_CONFIG.global.interface"); interface="${interface:-br-lan}"
local traffic_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark"); traffic_mark="${traffic_mark:-0x666}"
local routing_table=$(uci -q get "$UCI_CONFIG.global.routing_table"); routing_table="${routing_table:-888}"
local rule_priority=$(uci -q get "$UCI_CONFIG.global.rule_priority"); rule_priority="${rule_priority:-1000}"
# 1. 先尝试用旧记录清理一次,防止由于配置变更导致的残留
delete_route "quiet"
log_msg info "Setting up routing table $routing_table and rule for mark $traffic_mark with priority $rule_priority..."
# 2. 记录当前使用的标记值、路由表 ID 和优先级
echo "$traffic_mark:$routing_table:$rule_priority" > "$MARK_FILE"
# 3. 再次确保新标记和表的规则是干净的
while ip rule del fwmark "$traffic_mark" lookup "$routing_table" 2>/dev/null; do :; done
ip route flush table "$routing_table" 2>/dev/null
# 4. 添加路由和策略
local err
if ! err=$(ip route add default via "$proxy_server_ip_addr" dev "$interface" table "$routing_table" 2>&1); then
log_msg error "Failed to add default route: $err"
return 1
fi
err=$(ip rule add fwmark "$traffic_mark" lookup "$routing_table" priority "$rule_priority" 2>&1)
if [ $? -eq 0 ]; then
log_msg info "Route added: via $proxy_server_ip_addr dev $interface table $routing_table for mark $traffic_mark with priority $rule_priority"
return 0
else
log_msg error "Failed to add ip rule: $err"
return 1
fi
}
delete_route() {
local quiet="$1"
# 1. 获取两组清理目标:旧值(记录文件) 和 新值(当前UCI)
local old_mark="" old_table="" old_prio=""
if [ -f "$MARK_FILE" ]; then
local saved_val=$(cat "$MARK_FILE")
old_mark=$(echo "$saved_val" | cut -d':' -f1)
old_table=$(echo "$saved_val" | cut -d':' -f2)
old_prio=$(echo "$saved_val" | cut -d':' -f3)
[ "$quiet" != "quiet" ] && rm -f "$MARK_FILE"
fi
local curr_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark"); curr_mark="${curr_mark:-0x666}"
local curr_table=$(uci -q get "$UCI_CONFIG.global.routing_table"); curr_table="${curr_table:-888}"
local curr_prio=$(uci -q get "$UCI_CONFIG.global.rule_priority"); curr_prio="${curr_prio:-1000}"
[ "$quiet" != "quiet" ] && log_msg info "Cleaning up FlowProxy routes (Atomic Mode)..."
# 2. 核心清理函数:只清理带 fwmark 且指向特定 table 的规则
safe_del_rule() {
local m="$1" t="$2" p="$3"
[ -z "$m" ] || [ -z "$t" ] && return
log_msg debug "Attempting cleanup: mark=$m, table=$t, priority=${p:-any}"
# 精准清理 (带优先级)
[ -n "$p" ] && while ip rule del fwmark "$m" lookup "$t" priority "$p" 2>/dev/null; do
log_msg debug "Removed stale ip rule: mark $m lookup $t priority $p"
done
# 模糊清理 (不带优先级,防止配置中途被手动改动)
while ip rule del fwmark "$m" lookup "$t" 2>/dev/null; do
log_msg debug "Removed stale ip rule (fuzzy): mark $m lookup $t"
done
# 清空路由表
ip route flush table "$t" 2>/dev/null
log_msg debug "Flushed routing table $t"
}
# 执行清理:先清旧值,再清当前值 (防止残留)
safe_del_rule "$old_mark" "$old_table" "$old_prio"
[ "$old_mark" != "$curr_mark" ] || [ "$old_table" != "$curr_table" ] && \
safe_del_rule "$curr_mark" "$curr_table" "$curr_prio"
}
delete_nft() {
nft delete table inet flowproxy 2>/dev/null
log_msg info "Removed nftables table 'flowproxy'"
}
setup_dns() {
local enabled=$(uci -q get "$UCI_CONFIG.global.enabled")
local dns_proxy_enabled=$(uci -q get "$UCI_CONFIG.global.dns_proxy_enabled")
local proxy_server_dns_port=$(uci -q get "$UCI_CONFIG.global.proxy_server_dns_port"); proxy_server_dns_port="${proxy_server_dns_port:-5353}"
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
# Only apply DNS proxy if BOTH main service and DNS proxy are enabled
if [ "${enabled:-0}" != "1" ] || [ "${dns_proxy_enabled:-0}" != "1" ]; then
cleanup_dns
return 0
fi
[ -z "$proxy_server_ip_addr" ] && { log_msg error "proxy_server_ip_addr not configured, cannot setup DNS"; return 1; }
log_msg info "Setting up DNS redirection to $proxy_server_ip_addr#$proxy_server_dns_port..."
# Persistent backup in UCI
if ! uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then
local old_noresolv=$(uci -q get dhcp.@dnsmasq[0].noresolv)
local old_dns_redirect=$(uci -q get dhcp.@dnsmasq[0].dns_redirect)
local old_servers=$(uci -q get dhcp.@dnsmasq[0].server | tr '\n' ',' | sed 's/,$//')
log_msg debug "Backing up DNS: noresolv=${old_noresolv:-empty}, redirect=${old_dns_redirect:-empty}, servers=${old_servers:-empty}"
uci set "$UCI_CONFIG.dns_backup=dns_backup"
[ -n "$old_noresolv" ] && uci set "$UCI_CONFIG.dns_backup.noresolv=$old_noresolv"
[ -n "$old_dns_redirect" ] && uci set "$UCI_CONFIG.dns_backup.dns_redirect=$old_dns_redirect"
[ -n "$old_servers" ] && uci set "$UCI_CONFIG.dns_backup.servers=$old_servers"
uci commit "$UCI_CONFIG"
fi
log_msg debug "Applying new DNS: noresolv=1, redirect=0, server=$proxy_server_ip_addr#$proxy_server_dns_port"
uci set dhcp.@dnsmasq[0].noresolv='1'
uci set dhcp.@dnsmasq[0].dns_redirect='0'
# Delete current servers and add our proxy
uci -q delete dhcp.@dnsmasq[0].server
uci add_list dhcp.@dnsmasq[0].server="$proxy_server_ip_addr#$proxy_server_dns_port"
uci commit dhcp
/etc/init.d/dnsmasq restart
}
cleanup_dns() {
if uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then
log_msg info "Restoring original DNS configuration..."
local noresolv=$(uci -q get "$UCI_CONFIG.dns_backup.noresolv")
local dns_redirect=$(uci -q get "$UCI_CONFIG.dns_backup.dns_redirect")
local servers=$(uci -q get "$UCI_CONFIG.dns_backup.servers")
log_msg debug "Restoring DNS values: noresolv=${noresolv:-0}, redirect=${dns_redirect:-1}, servers=${servers:-empty}"
# Restore noresolv (default to 0 if not backed up)
[ -n "$noresolv" ] && uci set dhcp.@dnsmasq[0].noresolv="$noresolv" || uci set dhcp.@dnsmasq[0].noresolv="0"
# Restore dns_redirect (only restore if it originally existed, avoid polluting config)
if [ -n "$dns_redirect" ]; then
uci set dhcp.@dnsmasq[0].dns_redirect="$dns_redirect"
else
uci -q delete dhcp.@dnsmasq[0].dns_redirect
fi
# Restore servers correctly splitting comma-separated list
uci -q delete dhcp.@dnsmasq[0].server
if [ -n "$servers" ]; then
for s in $(echo "$servers" | tr ',' ' '); do
[ -n "$s" ] && uci add_list dhcp.@dnsmasq[0].server="$s"
done
fi
uci commit dhcp
uci delete "$UCI_CONFIG.dns_backup"
uci commit "$UCI_CONFIG"
/etc/init.d/dnsmasq restart
fi
}
do_degrade() {
local proxy_ip=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
log_msg warn "Proxy server ${proxy_ip:-unknown} unreachable, degrading FlowProxy to direct mode..."
# 先写入降级标记,避免 RPCD 状态自愈误删 STATE_FILE
touch "$DEGRADED_FILE"
delete_route
delete_nft
cleanup_dns
log_msg warn "FlowProxy degraded to direct mode (monitoring remains active)"
}
do_recover() {
local proxy_ip=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
log_msg info "Proxy server ${proxy_ip:-unknown} is online, recovering FlowProxy..."
restore_nft_rules || return 1
add_route || { delete_nft; return 1; }
setup_dns
rm -f "$DEGRADED_FILE"
log_msg info "FlowProxy recovered to active proxy mode"
return 0
}
+171
View File
@@ -0,0 +1,171 @@
#!/bin/sh
# FlowProxy sidecar watchdog daemon (ARP + Ping health probe)
. /usr/share/flowproxy/common.sh
MY_GEN="$1"
[ -z "$MY_GEN" ] && exit 0
STOP_REQUESTED=0
SLEEP_PID=""
handle_signal() {
STOP_REQUESTED=1
[ -n "$SLEEP_PID" ] && kill "$SLEEP_PID" 2>/dev/null
}
trap 'handle_signal' TERM INT
interruptible_sleep() {
local secs="$1"
[ "$STOP_REQUESTED" -eq 1 ] && return 1
sleep "$secs" &
SLEEP_PID=$!
wait "$SLEEP_PID" 2>/dev/null
SLEEP_PID=""
[ "$STOP_REQUESTED" -eq 1 ] && return 1
return 0
}
resolve_probe_iface() {
local iface="$1"
if [ -L "/sys/class/net/$iface/master" ]; then
local master_path=$(readlink "/sys/class/net/$iface/master" 2>/dev/null)
local master_name=$(basename "$master_path" 2>/dev/null)
[ -n "$master_name" ] && { echo "$master_name"; return; }
fi
echo "$iface"
}
get_uint_or_default() {
local val="$1"
local def="$2"
local min="$3"
case "$val" in
''|*[!0-9]*) echo "$def"; return ;;
esac
[ "$val" -lt "$min" ] && echo "$min" || echo "$val"
}
is_context_valid() {
[ "$STOP_REQUESTED" -eq 0 ] || return 1
[ -f "$STATE_FILE" ] || return 1
[ "$(cat "$GEN_FILE" 2>/dev/null)" = "$MY_GEN" ] || return 1
[ "$(uci -q get "$UCI_CONFIG.global.enabled" || echo 0)" = "1" ] || return 1
[ "$(uci -q get "$UCI_CONFIG.global.monitor_enabled" || echo 0)" = "1" ] || return 1
return 0
}
check_proxy_alive() {
local ip="$1"
local iface="$2"
# 1. ICMP Ping (首次成功即毫秒级返回;失败则补测一次防单包抖动)
if ping -c 1 -W 1 -I "$iface" "$ip" >/dev/null 2>&1; then
return 0
fi
if [ "$STOP_REQUESTED" -eq 1 ]; then
return 0
fi
if ping -c 1 -W 1 -I "$iface" "$ip" >/dev/null 2>&1; then
return 0
fi
# 2. ARP 互补探测 (防止旁路由禁 Ping 误报)
if command -v arping >/dev/null 2>&1; then
if arping -f -q -c 2 -w 1 -I "$iface" "$ip" >/dev/null 2>&1; then
return 0
fi
else
ip neigh flush to "$ip" dev "$iface" >/dev/null 2>&1
ping -c 1 -W 1 -I "$iface" "$ip" >/dev/null 2>&1
if ip neigh show to "$ip" dev "$iface" 2>/dev/null | grep -qE "REACHABLE|DELAY|PROBE"; then
return 0
fi
fi
return 1
}
proxy_ip=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr")
conf_iface=$(uci -q get "$UCI_CONFIG.global.interface")
conf_iface="${conf_iface:-br-lan}"
probe_iface=$(resolve_probe_iface "$conf_iface")
interval=$(get_uint_or_default "$(uci -q get "$UCI_CONFIG.global.monitor_interval")" 5 2)
fail_threshold=$(get_uint_or_default "$(uci -q get "$UCI_CONFIG.global.monitor_fail_threshold")" 3 1)
recover_threshold=$(get_uint_or_default "$(uci -q get "$UCI_CONFIG.global.monitor_recover_threshold")" 2 1)
[ -z "$proxy_ip" ] && exit 0
log_msg info "Watchdog started (target=$proxy_ip, iface=$probe_iface, interval=${interval}s, fail=$fail_threshold, recover=$recover_threshold)"
# 启动后先等待一个探测周期,确保主服务完全进入稳定运行态
interruptible_sleep "$interval" || exit 0
[ -f "$DEGRADED_FILE" ] && state="degraded" || state="online"
fail_count=0
ok_count=0
while is_context_valid; do
if check_proxy_alive "$proxy_ip" "$probe_iface"; then
fail_count=0
if [ "$state" = "degraded" ]; then
ok_count=$((ok_count + 1))
log_msg debug "Watchdog: probe succeeded ($ok_count/$recover_threshold)"
if [ "$ok_count" -ge "$recover_threshold" ]; then
acquire_lock
if ! is_context_valid; then
release_lock
break
fi
if [ -f "$DEGRADED_FILE" ]; then
if do_recover; then
state="online"
ok_count=0
fail_count=0
else
log_msg error "Watchdog: recovery failed, staying in degraded mode"
fi
else
state="online"
ok_count=0
fi
release_lock
fi
else
ok_count=0
fi
else
[ "$STOP_REQUESTED" -eq 1 ] && break
ok_count=0
if [ "$state" = "online" ]; then
fail_count=$((fail_count + 1))
log_msg warn "Watchdog: probe failed ($fail_count/$fail_threshold) for $proxy_ip via $probe_iface"
if [ "$fail_count" -ge "$fail_threshold" ]; then
acquire_lock
if ! is_context_valid; then
release_lock
break
fi
if [ ! -f "$DEGRADED_FILE" ]; then
do_degrade
state="degraded"
fail_count=0
ok_count=0
else
state="degraded"
fail_count=0
fi
release_lock
fi
else
fail_count=0
fi
fi
interruptible_sleep "$interval" || break
done
log_msg info "Watchdog stopped"
exit 0
@@ -6,6 +6,7 @@ import { connect } from 'ubus';
const UCI_CONFIG = 'flowproxy'; const UCI_CONFIG = 'flowproxy';
const STATE_FILE = '/var/run/flowproxy.running'; const STATE_FILE = '/var/run/flowproxy.running';
const DEGRADED_FILE = '/var/run/flowproxy.degraded';
const IP_CACHE = '/tmp/flowproxy/lan_ip.cache'; const IP_CACHE = '/tmp/flowproxy/lan_ip.cache';
const LOG_FILE = '/var/log/flowproxy.log'; const LOG_FILE = '/var/log/flowproxy.log';
const NFT_TABLE = 'flowproxy'; const NFT_TABLE = 'flowproxy';
@@ -20,6 +21,7 @@ const methods = {
call: function() { call: function() {
// 1. 快速探测 // 1. 快速探测
let is_running = access(STATE_FILE, 'f'); let is_running = access(STATE_FILE, 'f');
let is_degraded = access(DEGRADED_FILE, 'f');
// 2. 自愈:检查内核状态并同步标记文件 // 2. 自愈:检查内核状态并同步标记文件
let tcp_active = (system('nft list chain inet ' + NFT_TABLE + ' LAN_MARKFLOW_TCP >/dev/null 2>&1') === 0); let tcp_active = (system('nft list chain inet ' + NFT_TABLE + ' LAN_MARKFLOW_TCP >/dev/null 2>&1') === 0);
@@ -28,7 +30,7 @@ const methods = {
if (real_running && !is_running) { if (real_running && !is_running) {
try { writefile(STATE_FILE, ''); } catch(e) {} try { writefile(STATE_FILE, ''); } catch(e) {}
} else if (!real_running && is_running) { } else if (!real_running && is_running && !is_degraded) {
try { system('rm -f ' + STATE_FILE); } catch(e) {} try { system('rm -f ' + STATE_FILE); } catch(e) {}
} }
@@ -67,11 +69,15 @@ const methods = {
let proxy_server_ip_addr = uci_cursor.get(UCI_CONFIG, 'global', 'proxy_server_ip_addr') || ''; let proxy_server_ip_addr = uci_cursor.get(UCI_CONFIG, 'global', 'proxy_server_ip_addr') || '';
let dns_running = (uci_cursor.get(UCI_CONFIG, 'dns_backup') ? 1 : 0); let dns_running = (uci_cursor.get(UCI_CONFIG, 'dns_backup') ? 1 : 0);
let monitor_enabled = int(uci_cursor.get(UCI_CONFIG, 'global', 'monitor_enabled') || '0');
let degraded_active = (is_degraded && is_running && !real_running) ? 1 : 0;
return { return {
running: real_running ? 1 : 0, running: (real_running || degraded_active) ? 1 : 0,
degraded: degraded_active,
dns_running: dns_running, dns_running: dns_running,
enabled: int(uci_cursor.get(UCI_CONFIG, 'global', 'enabled') || '0'), enabled: int(uci_cursor.get(UCI_CONFIG, 'global', 'enabled') || '0'),
monitor_enabled: monitor_enabled,
proxy_server_ip_addr: proxy_server_ip_addr, proxy_server_ip_addr: proxy_server_ip_addr,
proxy_server_dns_port: uci_cursor.get(UCI_CONFIG, 'global', 'proxy_server_dns_port') || '5353', proxy_server_dns_port: uci_cursor.get(UCI_CONFIG, 'global', 'proxy_server_dns_port') || '5353',
lan_ip: safe_trim(lan_ip), lan_ip: safe_trim(lan_ip),
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box PKG_NAME:=luci-app-open-box
PKG_VERSION:=0.1.423 PKG_VERSION:=0.1.425
PKG_RELEASE:=73 PKG_RELEASE:=74
PKG_MAINTAINER:=liandu2024 PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
+9 -5
View File
@@ -10,8 +10,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-vantage PKG_NAME:=luci-app-vantage
PKG_VERSION:=1.0.19 PKG_VERSION:=1.0.20
PKG_RELEASE:=34 PKG_RELEASE:=35
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
LUCI_TITLE:=Vantage dashboard LUCI_TITLE:=Vantage dashboard
@@ -20,15 +20,19 @@ LUCI_DESCRIPTION:=Live network dashboard (landing page under admin/dashboard): d
Read-only except for station names and stations set not to warn about \ Read-only except for station names and stations set not to warn about \
their signal, which are kept in /etc/config/vantage. their signal, which are kept in /etc/config/vantage.
# rpcd-mod-iwinfo provides the station lists and the radios' state # rpcd-mod-iwinfo provides the station lists and the radios' state
# (iwinfo info/assoclist/survey). # (iwinfo info/assoclist).
# rpcd-mod-ucode runs the app's plugin (/usr/share/rpcd/ucode/luci.vantage: # rpcd-mod-ucode runs the app's plugin (/usr/share/rpcd/ucode/luci.vantage:
# key-free wireless status, thermal zones, validated alias writes), which # key-free wireless status, thermal zones, validated alias writes), which
# imports the uci, ubus and fs ucode modules; luci-base pulls all four in # imports the uci, ubus and fs ucode modules; luci-base pulls all four in
# already, they are listed because the plugin needs them. # already, they are listed because the plugin needs them. It also imports
# rtnl (the bridges' hosts, the default routes) and nl80211: it reads the
# radios' channel survey (the noise floor) through nl80211 itself, because
# rpcd's iwinfo survey crashes rpcd on drivers other than nl80211's (#124).
# Wi-Fi images already have both, as wifi-scripts needs them.
# Optional at runtime, degrade when absent: rpcd-mod-rrdns (reverse DNS # Optional at runtime, degrade when absent: rpcd-mod-rrdns (reverse DNS
# names), umdns (mDNS names), hostapd ubus objects (WPS names, client # names), umdns (mDNS names), hostapd ubus objects (WPS names, client
# capabilities). # capabilities).
LUCI_DEPENDS:=+luci-base +rpcd +rpcd-mod-iwinfo +rpcd-mod-ucode +ucode-mod-uci +ucode-mod-ubus +ucode-mod-fs +ucode-mod-rtnl LUCI_DEPENDS:=+luci-base +rpcd +rpcd-mod-iwinfo +rpcd-mod-ucode +ucode-mod-uci +ucode-mod-ubus +ucode-mod-fs +ucode-mod-rtnl +ucode-mod-nl80211
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
LUCI_MAINTAINER:=anothaDev <10740588+anothaDev@users.noreply.github.com> LUCI_MAINTAINER:=anothaDev <10740588+anothaDev@users.noreply.github.com>
LUCI_URL:=https://github.com/anothaDev/luci-theme-vantage LUCI_URL:=https://github.com/anothaDev/luci-theme-vantage
@@ -729,6 +729,13 @@ header.bg-primary {
font-size: var(--vt-fs-sm); color: var(--vt-text-2); margin-top: 2px; font-size: var(--vt-fs-sm); color: var(--vt-text-2); margin-top: 2px;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
} }
/* a health card's reason in its parts (overview.js): each part stays
whole, and its dot sits in the .9em before it (" · " is .89em). The
parts start .9em further left, under the clip, so a part that begins
a line hides its dot (#122) */
.vt-app .vt-chip-reason > .vt-parts { display: block; margin-inline-start: -.9em; }
.vt-app .vt-chip-reason .vt-part { display: inline-block; }
.vt-app .vt-chip-reason .vt-part::before { content: "·"; display: inline-block; width: .9em; text-align: center; }
/* -------------------------------------------------- 5. hero: network path */ /* -------------------------------------------------- 5. hero: network path */
@@ -892,12 +899,24 @@ header.bg-primary {
.vt-app .vt-branch-ssids .vt-node { max-width: 260px; } .vt-app .vt-branch-ssids .vt-node { max-width: 260px; }
/* a network has no second column: its rates take that room */ /* a network has no second column: its rates take that room */
.vt-app .vt-branch > .vt-node-net { grid-column: 1 / -1; justify-self: start; width: 100%; max-width: 260px; } .vt-app .vt-branch > .vt-node-net { grid-column: 1 / -1; justify-self: start; width: 100%; max-width: 260px; }
/* with subgrid every branch shares one radio column: 172px, or wider up
to 240px when the widest radio's own text needs it, so "Ch 128 · 160
MHz" in a wider font or a larger text size no longer ends in "…"
(#122). Without subgrid, the fixed column above. Narrower windows lay
the branches out as below. */
@supports (grid-template-columns: subgrid) {
.vt-app .vt-branches { display: grid; grid-template-columns: minmax(172px, max-content) minmax(0, 1fr); column-gap: 34px; row-gap: 18px; align-content: space-between; }
.vt-app .vt-branches > * { grid-column: 1 / -1; }
.vt-app .vt-branch { grid-template-columns: subgrid; }
.vt-app .vt-branch > .vt-node:first-child:not(.vt-node-net) { max-width: 240px; }
}
/* medium widths: the rates need their column, and the branches have room /* medium widths: the rates need their column, and the branches have room
to give; what a radio or the Wired node leads to goes under it, too to give; what a radio or the Wired node leads to goes under it, too
narrow beside it */ narrow beside it */
@container vthero (max-width: 1020px) { @container vthero (max-width: 1020px) {
.vt-app .vt-path { grid-template-columns: minmax(150px, 1.05fr) minmax(136px, .9fr) minmax(190px, 1.1fr) minmax(204px, 2fr); } .vt-app .vt-path { grid-template-columns: minmax(150px, 1.05fr) minmax(136px, .9fr) minmax(190px, 1.1fr) minmax(204px, 2fr); }
.vt-app .vt-branches { display: flex; }
.vt-app .vt-branch { grid-template-columns: minmax(0, 1fr); row-gap: 8px; } .vt-app .vt-branch { grid-template-columns: minmax(0, 1fr); row-gap: 8px; }
.vt-app .vt-branch > .vt-node { justify-self: start; min-width: min(200px, 100%); } .vt-app .vt-branch > .vt-node { justify-self: start; min-width: min(200px, 100%); }
.vt-app .vt-branch-ssids { padding-left: 34px; } .vt-app .vt-branch-ssids { padding-left: 34px; }
@@ -588,9 +588,10 @@ return baseclass.extend({
return out; return out;
}, },
/* iwinfo survey [ { mhz, noise, ... } ] -> the noise floor (dBm) on the /* luci.vantage survey [ { mhz, noise } ] -> the noise floor (dBm) on
channel at mhz, or null. Drivers send it as a signed byte, some as an the channel at mhz, or null. A signed byte, as nl80211 carries it;
unsigned one (ath12k: 146 for -110 dBm), and 0 when they have none */ rpcd's iwinfo survey (1.0.19) gave some drivers' as an unsigned one
(ath12k: 146 for -110 dBm); 0 when the driver has none */
surveyNoise: function(entries, mhz) { surveyNoise: function(entries, mhz) {
if (!mhz) return null; if (!mhz) return null;
var e = arr(entries).filter(function(x) { return x && num(x.mhz) === mhz; })[0]; var e = arr(entries).filter(function(x) { return x && num(x.mhz) === mhz; })[0];
@@ -38,8 +38,8 @@ var LOW_IDLE_POLL_MS = 30 * 1000;
IWINFO_MS, or at once when the interface's state moved (its radio up, IWINFO_MS, or at once when the interface's state moved (its radio up,
down, pending or reconfigured in luci.vantage wireless; its channel, down, pending or reconfigured in luci.vantage wireless; its channel,
frequency, BSSID or SSID in hostapd's get_status, asked every poll). frequency, BSSID or SSID in hostapd's get_status, asked every poll).
The noise floor, which moves, comes every poll from iwinfo survey The noise floor, which moves, comes every poll from luci.vantage
(1.5-2 ms a radio), its entry for the radio's channel */ survey (1.5-2.5 ms a radio), its entry for the radio's channel */
var IWINFO_MS = 30 * 1000; var IWINFO_MS = 30 * 1000;
/* luci-rpc getHostHints reads the DHCP leases and the ARP table: asked /* luci-rpc getHostHints reads the DHCP leases and the ARP table: asked
again after HINTS_MS, and on each of the HINTS_HOT polls after a again after HINTS_MS, and on each of the HINTS_HOT polls after a
@@ -70,7 +70,7 @@ var GONE_KEEP_MS = 5 * 60 * 1000; /* disconnected stations stay listed */
var TALKERS = 3; /* top talkers shown; the tile keeps room for all */ var TALKERS = 3; /* top talkers shown; the tile keeps room for all */
var SWITCH_MS = 30 * 1000; /* LuCI runs swconfig for each switch's link state: at most this often */ var SWITCH_MS = 30 * 1000; /* LuCI runs swconfig for each switch's link state: at most this often */
var STORE_KEY = 'vantage.hist.v1'; var STORE_KEY = 'vantage.hist.v1';
var VERSION = '1.0.19'; /* this package's, as its Makefile has it (tests/version.test.js) */ var VERSION = '1.0.20'; /* this package's, as its Makefile has it (tests/version.test.js) */
var SVGNS = 'http://www.w3.org/2000/svg'; var SVGNS = 'http://www.w3.org/2000/svg';
var HOSTAPD_OBJ = /^[A-Za-z0-9_.-]{1,64}$/; var HOSTAPD_OBJ = /^[A-Za-z0-9_.-]{1,64}$/;
@@ -98,8 +98,11 @@ var callHints = rpc.declare({ object: 'luci-rpc', method: 'getHostHints', expect
var callIwInfo = rpc.declare({ object: 'iwinfo', method: 'info', params: [ 'device' ], expect: { '': {} } }); var callIwInfo = rpc.declare({ object: 'iwinfo', method: 'info', params: [ 'device' ], expect: { '': {} } });
var callAssoc = rpc.declare({ object: 'iwinfo', method: 'assoclist', params: [ 'device' ], expect: { results: [] } }); var callAssoc = rpc.declare({ object: 'iwinfo', method: 'assoclist', params: [ 'device' ], expect: { results: [] } });
/* the channels' survey: the noise floor of the radio's own (model.js /* the channels' survey: the noise floor of the radio's own (model.js
surveyNoise); null when the call fails */ surveyNoise); null when the call fails. The plugin's, read through
var callSurvey = rpc.declare({ object: 'iwinfo', method: 'survey', params: [ 'device' ], expect: { results: [] } }); nl80211, not iwinfo's: rpcd's iwinfo survey crashes rpcd on every
iwinfo backend but nl80211's (MediaTek's mtwifi in the mt798x builds,
#124) */
var callSurvey = rpc.declare({ object: 'luci.vantage', method: 'survey', params: [ 'device' ], expect: { results: [] } });
var callRrdns = rpc.declare({ object: 'network.rrdns', method: 'lookup', params: [ 'addrs', 'timeout', 'limit' ], expect: { '': {} } }); var callRrdns = rpc.declare({ object: 'network.rrdns', method: 'lookup', params: [ 'addrs', 'timeout', 'limit' ], expect: { '': {} } });
var callMdns = rpc.declare({ object: 'umdns', method: 'hosts' }); var callMdns = rpc.declare({ object: 'umdns', method: 'hosts' });
var callUciGet = rpc.declare({ object: 'uci', method: 'get', params: [ 'config' ], expect: { values: {} } }); var callUciGet = rpc.declare({ object: 'uci', method: 'get', params: [ 'config' ], expect: { values: {} } });
@@ -2147,7 +2150,15 @@ return view.extend({
cell.check = c; cell.check = c;
txt(cell.title, c.title); txt(cell.title, c.title);
txt(cell.value, c.value); txt(cell.value, c.value);
txt(cell.reason, c.reason); /* the reason in its " · " parts: a part that does not fit goes
whole to the next line, without its dot (app.css), so "1 Gbit/s
link" never breaks after its "/" or inside a word (#122) */
var reason = c.reason == null ? '' : String(c.reason);
if (cell.reasonText !== reason) {
cell.reasonText = reason;
dom.content(cell.reason, reason === '' ? [] : E('span', { 'class': 'vt-parts' },
reason.split(' · ').map(function(p) { return E('span', { 'class': 'vt-part' }, [ p ]); })));
}
setTitle(cell.el, c.detail || c.reason); setTitle(cell.el, c.detail || c.reason);
patch(cell.viz, self.checkPicture(c)); patch(cell.viz, self.checkPicture(c));
}); });
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,6 @@
{ {
"luci-app-vantage": { "luci-app-vantage": {
"description": "Vantage dashboard: read-only status of the device, uplink, radios and stations (wireless configuration without keys, through luci.vantage wireless), its temperature sensors (luci.vantage thermal), the wired hosts its bridges have learned (luci.vantage wired: MAC, port and when last heard, and which switch ports a port's VLAN has), the link state of those switch ports (luci getSwconfigPortState) and the default route the kernel takes to the internet (luci.vantage routes: family, device and metric, no gateways)", "description": "Vantage dashboard: read-only status of the device, uplink, radios and stations (wireless configuration without keys, through luci.vantage wireless), its temperature sensors (luci.vantage thermal), the wired hosts its bridges have learned (luci.vantage wired: MAC, port and when last heard, and which switch ports a port's VLAN has), the link state of those switch ports (luci getSwconfigPortState), the default route the kernel takes to the internet (luci.vantage routes: family, device and metric, no gateways) and each radio's noise floor (luci.vantage survey: the channel survey read through nl80211)",
"read": { "read": {
"file": { "file": {
"/proc/stat": [ "read" ] "/proc/stat": [ "read" ]
@@ -12,8 +12,8 @@
"network.device": [ "status" ], "network.device": [ "status" ],
"luci-rpc": [ "getHostHints" ], "luci-rpc": [ "getHostHints" ],
"luci": [ "getSwconfigPortState" ], "luci": [ "getSwconfigPortState" ],
"luci.vantage": [ "routes", "thermal", "wired", "wireless" ], "luci.vantage": [ "routes", "survey", "thermal", "wired", "wireless" ],
"iwinfo": [ "assoclist", "info", "survey" ], "iwinfo": [ "assoclist", "info" ],
"hostapd.*": [ "get_clients", "get_status" ], "hostapd.*": [ "get_clients", "get_status" ],
"umdns": [ "hosts" ], "umdns": [ "hosts" ],
"uci": [ "get" ] "uci": [ "get" ]
@@ -43,6 +43,16 @@
// when a lookup could not be sent. One lookup a family, so // when a lookup could not be sent. One lookup a family, so
// the work does not grow with the routing table. Read group // the work does not grow with the routing table. Read group
// luci-app-vantage. // luci-app-vantage.
// survey { device }: the channel survey of one Wi-Fi interface, for
// the noise floor: { mhz, noise } (dBm) for each channel the
// driver gives a noise value, at most MAX_SURVEY: one
// read-only NL80211 survey dump. Not through rpcd's iwinfo
// survey: libiwinfo has a survey for its NL80211 backend
// only, and for any other (the MediaTek mtwifi backend of
// the mt798x builds) rpcd calls the missing function and
// dies (#124). Only an interface that cfg80211 drives (a
// phy80211 link in sysfs) is asked; any other device: no
// channels. Read group luci-app-vantage.
// set_alias { mac, name, icon }: name one station, or remove its name // set_alias { mac, name, icon }: name one station, or remove its name
// with name ''. Validated here, whatever the browser did, and // with name ''. Validated here, whatever the browser did, and
// written to /etc/config/vantage only. Write group // written to /etc/config/vantage only. Write group
@@ -60,8 +70,9 @@
import { cursor } from 'uci'; import { cursor } from 'uci';
import { connect } from 'ubus'; import { connect } from 'ubus';
import { lsdir, readfile } from 'fs'; import { access, lsdir, readfile } from 'fs';
import * as rtnl from 'rtnl'; import * as rtnl from 'rtnl';
import * as nl80211 from 'nl80211';
const CONFIG = 'vantage'; const CONFIG = 'vantage';
const WIRELESS = 'wireless'; // read only, by wireless_from_config() const WIRELESS = 'wireless'; // read only, by wireless_from_config()
@@ -85,11 +96,13 @@ const NETWORK = 'network'; // read only, by switches(): switch_vlan secti
const BOARD = '/etc/board.json'; const BOARD = '/etc/board.json';
const BOARD_MAX = 65536; // bytes read of it const BOARD_MAX = 65536; // bytes read of it
const MAX_SWITCH_VLANS = 64; const MAX_SWITCH_VLANS = 64;
const MAX_SURVEY = 256; // channels in a survey reply (a tri-band wiphy has about 100)
const MAX_MHZ = 100000; // a channel's frequency, MHz (60 GHz is 70200)
// Patterns, compiled once: ucode compiles a regular expression literal each // Patterns, compiled once: ucode compiles a regular expression literal each
// time the code reaches it, and a counted repeat ({1,64}) costs musl // time the code reaches it, and a counted repeat ({1,64}) costs musl
// milliseconds to compile on a router (8.5 ms against 0.07 ms for the same // milliseconds to compile on a router (8.5 ms against 0.07 ms for the same
// test with the pattern compiled once, on an ARM access point). rpcd keeps // test with the pattern compiled once, on an ARM Wi-Fi AP). rpcd keeps
// this module loaded, these with it, between calls. // this module loaded, these with it, between calls.
const RE_MAC_COLON = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/; const RE_MAC_COLON = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/;
const RE_MAC_DASH = /^[0-9a-f]{2}(-[0-9a-f]{2}){5}$/; const RE_MAC_DASH = /^[0-9a-f]{2}(-[0-9a-f]{2}){5}$/;
@@ -683,6 +696,39 @@ function routes() {
return out; return out;
} }
/* ------------------------------------------------------------- survey */
// The channel survey of one Wi-Fi interface: [ { mhz, noise } ] for each
// channel the driver reports with a noise value (dBm, a signed byte as
// NL80211 carries it; 0 is "none"), at most MAX_SURVEY. Asked of the
// kernel only for an interface cfg80211 drives (its phy80211 link): any
// other device would be refused by the kernel anyway, and is not asked.
// The reply of a dump is an array of messages; null when it had none or
// failed, false when it was cut short.
function survey(dev) {
let out = [];
if (!netdev(dev) || dev == '.' || dev == '..' || !access(NET + '/' + dev + '/phy80211'))
return out;
let res = nl80211.request(nl80211.const.NL80211_CMD_GET_SURVEY, nl80211.const.NLM_F_DUMP, { dev });
for (let m in ((type(res) == 'array') ? res : ((type(res) == 'object') ? [ res ] : []))) {
let s = (type(m) == 'object' && type(m.survey_info) == 'object') ? m.survey_info : null;
let mhz = s?.frequency, noise = s?.noise;
if (type(mhz) != 'int' || mhz <= 0 || mhz > MAX_MHZ || type(noise) != 'int' || noise == 0 || noise < -128 || noise > 127)
continue;
push(out, { mhz, noise });
if (length(out) >= MAX_SURVEY)
break;
}
return out;
}
// a switch port's number (0..63), or null // a switch port's number (0..63), or null
function switch_port(v) { function switch_port(v) {
return (type(v) == 'int' && v >= 0 && v < 64) ? v : null; return (type(v) == 'int' && v >= 0 && v < 64) ? v : null;
@@ -842,6 +888,13 @@ const methods = {
} }
}, },
survey: {
args: { device: 'device' },
call: function(request) {
return { results: survey(request.args?.device) };
}
},
set_alias: { set_alias: {
args: { mac: 'mac', name: 'name', icon: 'icon' }, args: { mac: 'mac', name: 'name', icon: 'icon' },
call: function(request) { call: function(request) {
+2 -2
View File
@@ -10,8 +10,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-theme-vantage PKG_NAME:=luci-theme-vantage
PKG_VERSION:=1.0.19 PKG_VERSION:=1.0.20
PKG_RELEASE:=43 PKG_RELEASE:=44
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
LUCI_TITLE:=Vantage theme LUCI_TITLE:=Vantage theme
@@ -56,7 +56,7 @@ const MOTION_EVENT = 'vantage:tick';
key, also what the dashboard reads under another theme) counts until key, also what the dashboard reads under another theme) counts until
the router has one. */ the router has one. */
/* this package's version, as its Makefile has it (tests/version.test.js) */ /* this package's version, as its Makefile has it (tests/version.test.js) */
const VERSION = '1.0.19'; const VERSION = '1.0.20';
/* Check for updates (Appearance): when asked, never on its own, the browser /* Check for updates (Appearance): when asked, never on its own, the browser
reads the project's 24.10 package list on GitHub Pages (the same release reads the project's 24.10 package list on GitHub Pages (the same release
as 25.12's), without cookies or a referrer. Only a version number passes; as 25.12's), without cookies or a referrer. Only a version number passes;
@@ -1939,6 +1939,10 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
.ifacebadge img { width: 16px; height: 16px; } .ifacebadge img { width: 16px; height: 16px; }
.ifacebadge.large { padding: 6px 10px; gap: 8px; font-size: var(--fs-base); } .ifacebadge.large { padding: 6px 10px; gap: 8px; font-size: var(--fs-base); }
.ifacebadge.large img { width: 20px; height: 20px; } .ifacebadge.large img { width: 20px; height: 20px; }
/* a network's status (Wireless, its dialog): the signal beside the details
keeps one width, wide enough for "-100/-110 dBm", so the details do not
move as the reading changes (#122) */
.ifacebadge.large > small:first-child { flex: none; min-width: 8.5em; text-align: center; }
.ifacebadge-active { border-color: var(--accent-line); box-shadow: inset 0 0 0 1px var(--accent-line); } .ifacebadge-active { border-color: var(--accent-line); box-shadow: inset 0 0 0 1px var(--accent-line); }
/* the device details tooltip (Interfaces): icon beside the details */ /* the device details tooltip (Interfaces): icon beside the details */
.cbi-tooltip.ifacebadge { display: inline-grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 10px; } .cbi-tooltip.ifacebadge { display: inline-grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 10px; }
@@ -2403,6 +2407,10 @@ body.modal-overlay-active #modal_overlay { display: block; }
#view .cbi-section > .cbi-title:has(+ div > .table:first-child), #view .cbi-section > .cbi-title:has(+ div > .table:first-child),
#view > .cbi-section > h3:first-child:has(+ div > .table:first-child), #view > .cbi-section > h3:first-child:has(+ div > .table:first-child),
#view .cbi-section:not(.cbi-tblsection, .cbi-section .cbi-section) > h3:first-child:has(+ .table) { margin-bottom: 0; } #view .cbi-section:not(.cbi-tblsection, .cbi-section .cbi-section) > h3:first-child:has(+ .table) { margin-bottom: 0; }
/* a table section whose table starts with its rows, no column titles
(Network -> Wireless): the band read as a taller first row (#122).
LuCI 25.12 adds a thead only for titles; 24.10 puts rows in the table */
#view .cbi-tblsection:not(.cbi-section .cbi-section) > h3:first-child:has(+ .table > :is(tbody, .tbody):first-child, + .table > .tr:first-child:not(.cbi-section-table-titles, .table-titles)) { margin-bottom: 0; }
#view > h2:not(:first-child), #view > h3:not(:first-child) { margin-top: var(--sp-6); } #view > h2:not(:first-child), #view > h3:not(:first-child) { margin-top: var(--sp-6); }
.cbi-section > .table + h3, .cbi-section > .table + .cbi-title, .cbi-section > div + h3 { margin-top: var(--sp-6); } .cbi-section > .table + h3, .cbi-section > .table + .cbi-title, .cbi-section > div + h3 { margin-top: var(--sp-6); }
.includes { margin-top: var(--sp-4); } .includes { margin-top: var(--sp-4); }
@@ -2757,18 +2765,23 @@ body[data-page="admin-statistics-graphs"] p.controls > .nowrap > .cbi-button { m
/* Network -> Wireless: radios as group headers, networks as rows */ /* Network -> Wireless: radios as group headers, networks as rows */
.td[data-name="_badge"] > .center { text-align: left; } .td[data-name="_badge"] > .center { text-align: left; }
/* the space between items is each item's own margin, not a gap: a
hidden " | " is still a flex item, and a gap around it indented a line
that starts with one (a network's second line, #122) */
.td[data-name="_stat"] > div, .td[data-name="_stat"] > div > div { .td[data-name="_stat"] > div, .td[data-name="_stat"] > div > div {
display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 20px; display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 0;
font-size: 0; /* hides the " | " separators */ font-size: 0; /* hides the " | " separators */
} }
.td[data-name="_stat"] > div > br, .td[data-name="_stat"] > div > div > br { display: none; } .td[data-name="_stat"] > div > br, .td[data-name="_stat"] > div > div > br { display: none; }
.td[data-name="_stat"] .nowrap { display: inline-flex; align-items: baseline; gap: 6px; font-size: var(--fs-base); } .td[data-name="_stat"] .nowrap { display: inline-flex; align-items: baseline; gap: 6px; margin-inline-end: 20px; font-size: var(--fs-base); }
.td[data-name="_stat"] > div > big { flex-basis: 100%; font-size: var(--fs-md); } .td[data-name="_stat"] > div > big { flex-basis: 100%; font-size: var(--fs-md); }
.td[data-name="_stat"] > div > div { flex-basis: 100%; } .td[data-name="_stat"] > div > div { flex-basis: 100%; }
.td[data-name="_stat"] > div > .nowrap:first-child { flex-basis: 100%; font-size: var(--fs-md); font-weight: 600; } .td[data-name="_stat"] > div > .nowrap:first-child { flex-basis: 100%; margin-inline-end: 0; font-size: var(--fs-md); font-weight: 600; }
.td[data-name="_stat"] > div > .nowrap:first-child > strong { display: none; } .td[data-name="_stat"] > div > .nowrap:first-child > strong { display: none; }
.cbi-section-table-row:has(> .td[data-name="_stat"] big) > .td { background: var(--s2); border-top: 1px solid var(--line-2); } .cbi-section-table-row:has(> .td[data-name="_stat"] big) > .td { background: var(--s2); }
.cbi-section-table-row:has(> .td[data-name="_stat"] big):first-child > .td { border-top: 0; } /* the darker line above a radio is the row above's own bottom border:
a top border of its own stacked under it as a double line (#122) */
.cbi-section-table-row:has(+ .cbi-section-table-row > .td[data-name="_stat"] big) > .td { border-bottom-color: var(--line-2); }
.cbi-section-table-row:not(:has(big)) > .td[data-name="_badge"] { padding-left: calc(var(--card-px) + 16px); } .cbi-section-table-row:not(:has(big)) > .td[data-name="_badge"] { padding-left: calc(var(--card-px) + 16px); }
/* progress bars (their text only exists in title): a thin track, the /* progress bars (their text only exists in title): a thin track, the
-49
View File
@@ -1,49 +0,0 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2025 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=pcat-mgr
PKG_RELEASE:=4
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://gitlab.com/CoiaPrant/pcat-mgr.git
PKG_SOURCE_DATE:=2025-08-01
PKG_SOURCE_VERSION:=754b4f0b783a44d48d91e2dbd6c1dfca667a3049
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-3.0-or-later
PKG_LICENSE_FILES:=LICENSE
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
PKG_FLAGS:=nonshared
PKG_BUILD_DEPENDS:=golang/host
PKG_BUILD_PARALLEL:=1
PKG_BUILD_FLAGS:=no-mips16
GO_PKG:=pcat-mgr
include $(INCLUDE_DIR)/package.mk
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
define Package/pcat-mgr
SECTION:=utils
CATEGORY:=Utilities
TITLE:=Photonicat Manager (Go Edition)
URL:=https://gitlab.com/CoiaPrant/pcat-mgr
DEPENDS:=$(GO_ARCH_DEPENDS) @TARGET_rockchip_armv8 +fuse-utils
CONFLICTS:=pcat-manager
endef
define Package/pcat-mgr/install
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/pcat-mgr $(1)/usr/sbin/pcat-mgr
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/pcat-mgr.init $(1)/etc/init.d/pcat-mgr
endef
$(eval $(call GoBinPackage,pcat-mgr))
$(eval $(call BuildPackage,pcat-mgr))
-13
View File
@@ -1,13 +0,0 @@
#!/bin/sh /etc/rc.common
START=45
USE_PROCD=1
start_service() {
procd_open_instance
procd_set_param command /usr/sbin/pcat-mgr
procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}