mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 20:08:05 +08:00
🎨 Sync 2026-11-12 18:00:39
This commit is contained in:
1 parent
47f4b4d6ab
commit
ed73e42a08
27 files changed
+1828
-1574
No files matched your search
+2
-2
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gately
|
||||
PKG_VERSION:=0.6.5.5
|
||||
PKG_RELEASE:=32
|
||||
PKG_VERSION:=0.6.6
|
||||
PKG_RELEASE:=33
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
+3
-3
@@ -5,11 +5,11 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=honk
|
||||
PKG_VERSION:=2026.11.09~6484888
|
||||
PKG_RELEASE:=34
|
||||
PKG_VERSION:=2026.11.12~a09d3bf
|
||||
PKG_RELEASE:=35
|
||||
|
||||
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_FILES:=LICENSE
|
||||
|
||||
@@ -10,10 +10,8 @@ LUCI_DEPENDS:=+fastnet +luci-lib-linkeaseauth +linkease-app-entry
|
||||
# APK uses -rN; OPKG uses -N for runtime package releases.
|
||||
LUCI_EXTRA_DEPENDS:=fastnet (>=0.8.9-$(if $(CONFIG_USE_APK),r)1)
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_VERSION:=0.8.9-r1
|
||||
PKG_RELEASE:=8
|
||||
# Preserve LuCI translation version ordering when upgrading older git-* packages.
|
||||
PKG_PO_VERSION:=git-26.313.00000-082
|
||||
PKG_VERSION:=0.8.9-r2
|
||||
PKG_RELEASE:=9
|
||||
LUCI_MINIFY_CSS:=0
|
||||
LUCI_MINIFY_JS:=0
|
||||
|
||||
|
||||
@@ -490,42 +490,39 @@ function flagToBool(flag) {
|
||||
|
||||
function renderPayload(s, total, uciconfig) {
|
||||
// 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) {
|
||||
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) {
|
||||
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});
|
||||
|
||||
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
||||
UIEl.setValue(rule.toString('json'));
|
||||
this.section.getOption('entry').instset(section_id, L.bind(onchange_callback, this, n, key, value));
|
||||
}
|
||||
o.write = function() {};
|
||||
o.rmempty = false;
|
||||
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.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) {
|
||||
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 limit = rule.getPayloads().length;
|
||||
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'));
|
||||
this.section.getOption('entry').instset(section_id, L.bind(onchange_callback, this, n, key, value));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -535,6 +532,7 @@ function renderPayload(s, total, uciconfig) {
|
||||
}
|
||||
|
||||
let o, prefix;
|
||||
|
||||
// StaticList payload
|
||||
for (let n=0; n<total; n++) {
|
||||
prefix = `payload${n}_`;
|
||||
@@ -549,15 +547,6 @@ function renderPayload(s, total, uciconfig) {
|
||||
o.depends('type', key);
|
||||
})
|
||||
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}`);
|
||||
if (n === 0) {
|
||||
@@ -608,48 +597,40 @@ function renderPayload(s, total, uciconfig) {
|
||||
if (n === 0)
|
||||
o.depends('type', 'IN-TYPE');
|
||||
o.depends(prefix + 'type', 'IN-TYPE');
|
||||
initPayload(o, n, 'factor');
|
||||
o.load = L.bind(function(n, key, uciconfig, section_id) {
|
||||
return (new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key] || '')
|
||||
.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'));
|
||||
}
|
||||
initPayload(o, n, 'factor',
|
||||
(n, k, inst) => (inst.getPayload(n)[k] || '').split('/'),
|
||||
(n, k, v, inst) => inst.setPayload(n, Object.fromEntries([[k, v.join('/')]]))
|
||||
);
|
||||
|
||||
o = s.option(form.ListValue, prefix + 'inname', _('Factor') + ` ${n+1}`);
|
||||
if (n === 0)
|
||||
o.depends('type', 'IN-NAME');
|
||||
o.depends(prefix + 'type', 'IN-NAME');
|
||||
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, [
|
||||
['', _('-- Please choose --')],
|
||||
...hm.loadLabelValues(this.config, 'inbound')
|
||||
], section_id);
|
||||
|
||||
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key];
|
||||
}, o, n, 'factor', uciconfig)
|
||||
return inname_old_load.apply(this, arguments);
|
||||
}
|
||||
|
||||
o = s.option(form.ListValue, prefix + 'rematchname', _('Factor') + ` ${n+1}`);
|
||||
if (n === 0)
|
||||
o.depends('type', 'REMATCH-NAME');
|
||||
o.depends(prefix + 'type', 'REMATCH-NAME');
|
||||
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, [
|
||||
['', _('-- Please choose --')],
|
||||
...hm.loadLabelValues(this.config, 'rematch-name')
|
||||
], section_id);
|
||||
|
||||
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key];
|
||||
}, o, n, 'factor', uciconfig)
|
||||
return rematchname_old_load.apply(this, arguments);
|
||||
}
|
||||
|
||||
o = s.option(form.Value, prefix + 'uint', _('Factor') + ` ${n+1}`);
|
||||
o.datatype = 'uinteger';
|
||||
@@ -678,28 +659,20 @@ function renderPayload(s, total, uciconfig) {
|
||||
o.depends('type', 'RULE-SET');
|
||||
o.depends(prefix + 'type', 'RULE-SET');
|
||||
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);
|
||||
|
||||
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key];
|
||||
}, o, n, 'factor', uciconfig)
|
||||
return rule_set_old_load.apply(this, arguments);
|
||||
}
|
||||
|
||||
o = s.option(form.Flag, prefix + 'NOT', _('NOT') + ` ${n+1}`);
|
||||
o.default = o.disabled;
|
||||
o.depends(Object.fromEntries([[prefix + 'type', /.+/]]));
|
||||
initPayload(o, n, 'deny');
|
||||
o.load = L.bind(function(n, key, uciconfig, section_id) {
|
||||
return boolToFlag(new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key] ? true : false);
|
||||
}, 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'));
|
||||
}
|
||||
initPayload(o, n, 'deny',
|
||||
(n, k, inst) => boolToFlag(inst.getPayload(n)[k] ? true : false),
|
||||
(n, k, v, inst) => inst.setPayload(n, Object.fromEntries([[k, flagToBool(v) || null]]))
|
||||
);
|
||||
}
|
||||
|
||||
// DynamicList payload
|
||||
@@ -721,23 +694,15 @@ function renderPayload(s, total, uciconfig) {
|
||||
extenbox[n].forEach((type) => {
|
||||
o.depends('type', type);
|
||||
})
|
||||
initDynamicPayload(o, n, 'type');
|
||||
o.validate = function(section_id, value) {
|
||||
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];
|
||||
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;
|
||||
}
|
||||
initDynamicPayload(o, n, 'type',
|
||||
undefined,
|
||||
(n, k, v, inst) => {
|
||||
v.forEach((val) => {
|
||||
inst.setPayload(n, Object.fromEntries([[k, val]])); n++;
|
||||
});
|
||||
inst.setPayload(n, Object.fromEntries([[k, null]]), n);
|
||||
}
|
||||
);
|
||||
|
||||
o = s.option(hm.GroupDynamicList, prefix + 'fused', _('Factor') + ' ++',
|
||||
_('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', /.+/]]));
|
||||
})
|
||||
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.inbound_type.map(e => [e, _('IN-TYPE')]), // Multi-values are not supported
|
||||
...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')])
|
||||
], section_id);
|
||||
|
||||
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayloads().slice(n).map(e => e[key] ?? '');
|
||||
}, o, n, 'factor', uciconfig)
|
||||
return fused_old_load.apply(this, arguments);
|
||||
}
|
||||
|
||||
o = s.option(hm.StaticList, prefix + 'NOTs', _('NOT') + ' ++',
|
||||
_('<code>0</code> or <code>1</code> only.'));
|
||||
@@ -765,27 +731,16 @@ function renderPayload(s, total, uciconfig) {
|
||||
extenbox[n].forEach((type) => {
|
||||
o.depends(Object.fromEntries([['type', type], [prefix + 'type', /.+/]]));
|
||||
})
|
||||
initDynamicPayload(o, n, 'deny');
|
||||
o.load = L.bind(function(n, key, uciconfig, section_id) {
|
||||
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayloads().slice(n).map(e => boolToFlag(e[key] ? true : false));
|
||||
}, o, n, 'deny', uciconfig);
|
||||
o.validate = function(section_id, value) {
|
||||
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 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;
|
||||
}
|
||||
initDynamicPayload(o, n, 'deny',
|
||||
(n, k, inst) => inst.getPayloads().slice(n).map(e => boolToFlag(e[k] ? true : false)),
|
||||
(n, k, v, inst) => {
|
||||
const limit = inst.getPayloads().length;
|
||||
v.forEach((val) => {
|
||||
inst.setPayload(n, Object.fromEntries([[k, flagToBool(val) || null]])); n++;
|
||||
});
|
||||
inst.setPayload(limit, Object.fromEntries([[k, null]]), limit);
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -801,94 +756,90 @@ function renderRules(s, uciconfig) {
|
||||
return El;
|
||||
}
|
||||
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.remove = form.AbstractValue.prototype.remove;
|
||||
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.default = hm.rules_type[0].types[0][0];
|
||||
for (const {label, types} of hm.rules_type.concat(hm.rules_logical_type)) {
|
||||
types.forEach((res) => o.value.call(o, res, label));
|
||||
}
|
||||
o.load = function(section_id) {
|
||||
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).type;
|
||||
}
|
||||
initEntryOption(o, 'type');
|
||||
o.validate = function(section_id, value) {
|
||||
// params only available for types other than
|
||||
if (hm.rules_type_allowparms.includes(value)) {
|
||||
['no-resolve', 'src'].forEach((opt) => {
|
||||
let UIEl = this.section.getUIElement(section_id, opt);
|
||||
const params = ['no-resolve', 'src'];
|
||||
for (let param of params) {
|
||||
let UIEl = this.section.getUIElement(section_id, param);
|
||||
|
||||
if (hm.rules_type_allowparms.includes(value))
|
||||
UIEl.node.querySelector('input').removeAttribute('disabled');
|
||||
});
|
||||
} else {
|
||||
['no-resolve', 'src'].forEach((opt) => {
|
||||
let UIEl = this.section.getUIElement(section_id, opt);
|
||||
else {
|
||||
UIEl.setValue('');
|
||||
UIEl.node.querySelector('input').disabled = true;
|
||||
});
|
||||
|
||||
let UIEl = this.section.getUIElement(section_id, 'entry');
|
||||
|
||||
let rule = new RulesEntry(UIEl.getValue()).setParam('no-resolve').setParam('src');
|
||||
|
||||
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
||||
UIEl.setValue(rule.toString('json'));
|
||||
this.section.getOption('entry').instset(section_id, inst => params.forEach(param => inst.setParam(param)));
|
||||
}
|
||||
}
|
||||
|
||||
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.modalonly = true;
|
||||
|
||||
renderPayload(s, Math.max(...Object.values(hm.rules_logical_payload_count).map(e => e.low)), uciconfig);
|
||||
|
||||
o = s.option(hm.ListValue, 'detour', _('Proxy group'));
|
||||
initEntryOption(o, 'detour');
|
||||
const detour_old_load = o.load;
|
||||
o.load = function(section_id) {
|
||||
hm.loadLabel.call(this, [
|
||||
...hm.preset_outbound.full,
|
||||
...hm.loadLabelValues(this.config, 'proxy_group')
|
||||
], 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.editable = true;
|
||||
|
||||
o = s.option(form.Flag, 'src', _('src'),
|
||||
_('Treat the <code>destination IP</code> as the <code>source IP</code>.'));
|
||||
o.default = o.disabled;
|
||||
o.load = function(section_id) {
|
||||
return boolToFlag(new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getParam('src') ? true : false);
|
||||
}
|
||||
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() {};
|
||||
initEntryOption(o, 'src',
|
||||
(k, inst) => boolToFlag(inst.getParam(k) ? true : false),
|
||||
(k, v, inst) => inst.setParam(k, flagToBool(v) || null)
|
||||
);
|
||||
o.depends('SUB-RULE', '');
|
||||
o.modalonly = true;
|
||||
|
||||
@@ -896,18 +847,10 @@ function renderRules(s, uciconfig) {
|
||||
_('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'));
|
||||
o.default = o.disabled;
|
||||
o.load = function(section_id) {
|
||||
return boolToFlag(new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getParam('no-resolve') ? true : false);
|
||||
}
|
||||
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() {};
|
||||
initEntryOption(o, 'no-resolve',
|
||||
(k, inst) => boolToFlag(inst.getParam(k) ? true : false),
|
||||
(k, v, inst) => inst.setParam(k, flagToBool(v) || null)
|
||||
);
|
||||
o.depends('SUB-RULE', '');
|
||||
o.modalonly = true;
|
||||
}
|
||||
@@ -1421,7 +1364,10 @@ return view.extend({
|
||||
...hm.loadLabelValues(this.config, 'subrule-group')
|
||||
], 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) {
|
||||
let UIEl = this.section.getUIElement(section_id, 'detour');
|
||||
@@ -1431,14 +1377,6 @@ return view.extend({
|
||||
|
||||
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.modalonly = true;
|
||||
/* Routing rules END */
|
||||
@@ -1642,85 +1580,84 @@ return view.extend({
|
||||
so.editable = true;
|
||||
|
||||
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.remove = form.AbstractValue.prototype.remove;
|
||||
so.editable = true;
|
||||
|
||||
so = ss.option(form.Value, 'addr', _('Address'));
|
||||
so.load = function(section_id) {
|
||||
return new DNSAddress(uci.get(data[0], section_id, 'address')).addr;
|
||||
// common part
|
||||
const initAddressOption = function(o, key,
|
||||
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) {
|
||||
if (value.match('#'))
|
||||
return _('Expecting: %s').format(_('No add\'l params'));
|
||||
|
||||
// params only available on DoH
|
||||
// https://github.com/muink/mihomo/blob/43f21c0b412b7a8701fe7a2ea6510c5b985a53d6/config/config.go#L1211C8-L1211C14
|
||||
if (value.match(/^https?:\/\//)){
|
||||
this.section.getUIElement(section_id, 'h3').node.querySelector('input').removeAttribute('disabled');
|
||||
} else {
|
||||
let UIEl = this.section.getUIElement(section_id, 'address');
|
||||
let UIEl = this.section.getUIElement(section_id, 'h3');
|
||||
if (value.match(/^https?:\/\//))
|
||||
UIEl.node.querySelector('input').removeAttribute('disabled');
|
||||
else {
|
||||
this.section.getOption('address').instset(section_id, inst => inst.setParam('h3'));
|
||||
|
||||
let newvalue = new DNSAddress(UIEl.getValue()).setParam('h3').toString();
|
||||
|
||||
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;
|
||||
});
|
||||
UIEl.setValue('');
|
||||
UIEl.node.querySelector('input').disabled = 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.modalonly = true;
|
||||
|
||||
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) {
|
||||
hm.loadLabel.call(this, [
|
||||
...hm.preset_outbound.dns,
|
||||
...hm.loadLabelValues(this.config, 'proxy_group')
|
||||
], 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 = ss.option(form.Flag, 'h3', _('HTTP/3'));
|
||||
so.default = so.disabled;
|
||||
so.load = function(section_id) {
|
||||
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() {};
|
||||
initAddressOption(so, 'h3');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.Flag, 'skip-cert-verify', _('Skip cert verify'),
|
||||
@@ -1728,101 +1665,42 @@ return view.extend({
|
||||
'<br/>' +
|
||||
_('This is <strong>DANGEROUS</strong>, your traffic is almost like <strong>PLAIN TEXT</strong>! Use at your own risk!'));
|
||||
so.default = so.disabled;
|
||||
so.load = function(section_id) {
|
||||
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() {};
|
||||
initAddressOption(so, 'skip-cert-verify');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.Value, 'ecs', _('EDNS Client Subnet'));
|
||||
so.datatype = 'cidr';
|
||||
so.load = function(section_id) {
|
||||
return new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('ecs');
|
||||
}
|
||||
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() {};
|
||||
initAddressOption(so, 'ecs',
|
||||
(k, inst) => inst.parseParam(k),
|
||||
(k, v, inst) => inst.setParam(k, v)
|
||||
);
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.Flag, 'ecs-override', _('ECS override'),
|
||||
_('Override the existing ECS in original request.'));
|
||||
so.default = so.disabled;
|
||||
so.load = function(section_id) {
|
||||
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() {};
|
||||
initAddressOption(so, 'ecs-override');
|
||||
so.depends({'ecs': /.+/});
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.Flag, 'disable-ipv4', _('Filter record: %s').format('A'));
|
||||
so.default = so.disabled;
|
||||
so.load = function(section_id) {
|
||||
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);
|
||||
}
|
||||
initAddressOption(so, 'disable-ipv4');
|
||||
so.write = function() {};
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.Flag, 'disable-ipv6', _('Filter record: %s').format('AAAA'));
|
||||
so.default = so.disabled;
|
||||
so.load = function(section_id) {
|
||||
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() {};
|
||||
initAddressOption(so, 'disable-ipv6');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'disable-qtypes', _('Filter record type:'));
|
||||
so.datatype = 'uinteger';
|
||||
so.placeholder = '65';
|
||||
so.load = function(section_id) {
|
||||
return new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('disable-qtypes');
|
||||
}
|
||||
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() {};
|
||||
initAddressOption(so, 'disable-qtypes',
|
||||
(k, inst) => inst.parseParam(k),
|
||||
(k, v, inst) => inst.setParam(k, v)
|
||||
);
|
||||
so.modalonly = true;
|
||||
/* DNS server END */
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-flowproxy
|
||||
PKG_VERSION:=1.0.3
|
||||
PKG_RELEASE:=6
|
||||
PKG_RELEASE:=7
|
||||
|
||||
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
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
|
||||
@@ -75,12 +75,21 @@ return L.view.extend({
|
||||
return callGetStatus().then(L.bind(function(status) {
|
||||
if (!status) return;
|
||||
var isRunning = (status.running == 1);
|
||||
var isDegraded = (status.degraded == 1);
|
||||
var container = node || document;
|
||||
|
||||
var statusEl = container.querySelector('#service-status');
|
||||
if (statusEl) {
|
||||
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 = [];
|
||||
if (status.nft && status.nft.tcp) chains.push('TCP');
|
||||
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, '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.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?"
|
||||
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)."
|
||||
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 rule_priority '1000'
|
||||
option dns_proxy_enabled '0'
|
||||
option monitor_enabled '0'
|
||||
option proxy_server_dns_port '5353'
|
||||
option log_level 'info'
|
||||
option log_size '1024'
|
||||
|
||||
@@ -5,299 +5,86 @@ START=99
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
NAME="flowproxy"
|
||||
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"
|
||||
. /usr/share/flowproxy/common.sh
|
||||
|
||||
get_level_weight() {
|
||||
case "$1" in
|
||||
debug) echo 0 ;;
|
||||
info) echo 1 ;;
|
||||
warn) echo 2 ;;
|
||||
error) echo 3 ;;
|
||||
*) echo 1 ;;
|
||||
esac
|
||||
_stop_unlocked() {
|
||||
log_msg info "Stopping FlowProxy..."
|
||||
# 强制先移除标记,防止状态误报及旧守护进程继续动作
|
||||
rm -f "$STATE_FILE" "$DEGRADED_FILE" "$GEN_FILE"
|
||||
delete_route
|
||||
delete_nft
|
||||
cleanup_dns
|
||||
rm -f "$NFT_CONF"
|
||||
log_msg info "FlowProxy service stopped"
|
||||
}
|
||||
|
||||
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" || echo "info")
|
||||
local conf_weight=$(get_level_weight "$conf_level")
|
||||
local msg_weight=$(get_level_weight "$level")
|
||||
[ "$msg_weight" -lt "$conf_weight" ] && return
|
||||
_start_unlocked() {
|
||||
local enabled=$(uci -q get "$UCI_CONFIG.global.enabled")
|
||||
|
||||
# 简单的日志截断逻辑,防止日志无限增长
|
||||
# 读取配置的大小限制,默认 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
|
||||
if [ "${enabled:-0}" != "1" ]; then
|
||||
log_msg info "FlowProxy is disabled, ensuring service is stopped"
|
||||
_stop_unlocked
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "[$timestamp] [$level] $msg" >> "$LOG_FILE"
|
||||
}
|
||||
log_msg info "Starting FlowProxy..."
|
||||
|
||||
check_env() {
|
||||
# 检测核心二进制工具是否存在
|
||||
if ! command -v nft >/dev/null 2>&1; then
|
||||
log_msg error "nftables binary not found"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
check_env || return 1
|
||||
check_config || return 1
|
||||
apply_nft_rules || return 1
|
||||
add_route || { delete_nft; return 1; }
|
||||
setup_dns
|
||||
|
||||
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
|
||||
}
|
||||
rm -f "$DEGRADED_FILE"
|
||||
touch "$STATE_FILE"
|
||||
|
||||
apply_nft_rules() {
|
||||
log_msg info "Generating nftables configuration..."
|
||||
local conf_level=$(uci -q get "$UCI_CONFIG.global.log_level" || echo "info")
|
||||
local gen="$(date +%s)-$$"
|
||||
echo "$gen" > "$GEN_FILE"
|
||||
|
||||
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"
|
||||
rm -f "$err_file"
|
||||
fi
|
||||
local monitor_enabled=$(uci -q get "$UCI_CONFIG.global.monitor_enabled" || echo 0)
|
||||
if [ "$monitor_enabled" = "1" ]; then
|
||||
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
|
||||
|
||||
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
|
||||
log_msg info "FlowProxy service started successfully"
|
||||
return 0
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local enabled=$(uci -q get "$UCI_CONFIG.global.enabled" || echo 0)
|
||||
|
||||
if [ "$enabled" -eq 0 ]; then
|
||||
log_msg info "FlowProxy is disabled, ensuring service is stopped"
|
||||
stop_service
|
||||
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"
|
||||
acquire_lock
|
||||
_start_unlocked
|
||||
local ret=$?
|
||||
release_lock
|
||||
return $ret
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
log_msg info "Stopping FlowProxy..."
|
||||
# 强制先移除标记,防止状态误报
|
||||
rm -f "$STATE_FILE"
|
||||
delete_route
|
||||
delete_nft
|
||||
cleanup_dns
|
||||
log_msg info "FlowProxy service stopped"
|
||||
acquire_lock
|
||||
_stop_unlocked
|
||||
local ret=$?
|
||||
release_lock
|
||||
return $ret
|
||||
}
|
||||
|
||||
restart_service() {
|
||||
stop_service
|
||||
start_service
|
||||
acquire_lock
|
||||
_stop_unlocked
|
||||
_start_unlocked
|
||||
local ret=$?
|
||||
release_lock
|
||||
return $ret
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
restart_service
|
||||
restart_service
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
# 监听自身配置改动
|
||||
procd_add_reload_trigger "$UCI_CONFIG"
|
||||
# 监听防火墙重载事件,防止规则被 fw4 清空后无法自动恢复
|
||||
procd_add_reload_trigger "firewall"
|
||||
# 监听自身配置改动
|
||||
procd_add_reload_trigger "$UCI_CONFIG"
|
||||
# 监听防火墙重载事件,防止规则被 fw4 清空后无法自动恢复
|
||||
procd_add_reload_trigger "firewall"
|
||||
}
|
||||
+328
@@ -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
@@ -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 STATE_FILE = '/var/run/flowproxy.running';
|
||||
const DEGRADED_FILE = '/var/run/flowproxy.degraded';
|
||||
const IP_CACHE = '/tmp/flowproxy/lan_ip.cache';
|
||||
const LOG_FILE = '/var/log/flowproxy.log';
|
||||
const NFT_TABLE = 'flowproxy';
|
||||
@@ -20,6 +21,7 @@ const methods = {
|
||||
call: function() {
|
||||
// 1. 快速探测
|
||||
let is_running = access(STATE_FILE, 'f');
|
||||
let is_degraded = access(DEGRADED_FILE, 'f');
|
||||
|
||||
// 2. 自愈:检查内核状态并同步标记文件
|
||||
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) {
|
||||
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) {}
|
||||
}
|
||||
|
||||
@@ -67,11 +69,15 @@ const methods = {
|
||||
|
||||
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 monitor_enabled = int(uci_cursor.get(UCI_CONFIG, 'global', 'monitor_enabled') || '0');
|
||||
let degraded_active = (is_degraded && is_running && !real_running) ? 1 : 0;
|
||||
|
||||
return {
|
||||
running: real_running ? 1 : 0,
|
||||
running: (real_running || degraded_active) ? 1 : 0,
|
||||
degraded: degraded_active,
|
||||
dns_running: dns_running,
|
||||
enabled: int(uci_cursor.get(UCI_CONFIG, 'global', 'enabled') || '0'),
|
||||
monitor_enabled: monitor_enabled,
|
||||
proxy_server_ip_addr: proxy_server_ip_addr,
|
||||
proxy_server_dns_port: uci_cursor.get(UCI_CONFIG, 'global', 'proxy_server_dns_port') || '5353',
|
||||
lan_ip: safe_trim(lan_ip),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-open-box
|
||||
PKG_VERSION:=0.1.423
|
||||
PKG_RELEASE:=73
|
||||
PKG_VERSION:=0.1.425
|
||||
PKG_RELEASE:=74
|
||||
|
||||
PKG_MAINTAINER:=liandu2024
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-vantage
|
||||
PKG_VERSION:=1.0.19
|
||||
PKG_RELEASE:=34
|
||||
PKG_VERSION:=1.0.20
|
||||
PKG_RELEASE:=35
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
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 \
|
||||
their signal, which are kept in /etc/config/vantage.
|
||||
# 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:
|
||||
# key-free wireless status, thermal zones, validated alias writes), which
|
||||
# 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
|
||||
# names), umdns (mDNS names), hostapd ubus objects (WPS names, client
|
||||
# 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_MAINTAINER:=anothaDev <10740588+anothaDev@users.noreply.github.com>
|
||||
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;
|
||||
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 */
|
||||
|
||||
@@ -892,12 +899,24 @@ header.bg-primary {
|
||||
.vt-app .vt-branch-ssids .vt-node { max-width: 260px; }
|
||||
/* 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; }
|
||||
/* 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
|
||||
to give; what a radio or the Wired node leads to goes under it, too
|
||||
narrow beside it */
|
||||
@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-branches { display: flex; }
|
||||
.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-ssids { padding-left: 34px; }
|
||||
|
||||
@@ -588,9 +588,10 @@ return baseclass.extend({
|
||||
return out;
|
||||
},
|
||||
|
||||
/* iwinfo survey [ { mhz, noise, ... } ] -> the noise floor (dBm) on the
|
||||
channel at mhz, or null. Drivers send it as a signed byte, some as an
|
||||
unsigned one (ath12k: 146 for -110 dBm), and 0 when they have none */
|
||||
/* luci.vantage survey [ { mhz, noise } ] -> the noise floor (dBm) on
|
||||
the channel at mhz, or null. A signed byte, as nl80211 carries it;
|
||||
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) {
|
||||
if (!mhz) return null;
|
||||
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,
|
||||
down, pending or reconfigured in luci.vantage wireless; its channel,
|
||||
frequency, BSSID or SSID in hostapd's get_status, asked every poll).
|
||||
The noise floor, which moves, comes every poll from iwinfo survey
|
||||
(1.5-2 ms a radio), its entry for the radio's channel */
|
||||
The noise floor, which moves, comes every poll from luci.vantage
|
||||
survey (1.5-2.5 ms a radio), its entry for the radio's channel */
|
||||
var IWINFO_MS = 30 * 1000;
|
||||
/* 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
|
||||
@@ -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 SWITCH_MS = 30 * 1000; /* LuCI runs swconfig for each switch's link state: at most this often */
|
||||
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 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 callAssoc = rpc.declare({ object: 'iwinfo', method: 'assoclist', params: [ 'device' ], expect: { results: [] } });
|
||||
/* the channels' survey: the noise floor of the radio's own (model.js
|
||||
surveyNoise); null when the call fails */
|
||||
var callSurvey = rpc.declare({ object: 'iwinfo', method: 'survey', params: [ 'device' ], expect: { results: [] } });
|
||||
surveyNoise); null when the call fails. The plugin's, read through
|
||||
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 callMdns = rpc.declare({ object: 'umdns', method: 'hosts' });
|
||||
var callUciGet = rpc.declare({ object: 'uci', method: 'get', params: [ 'config' ], expect: { values: {} } });
|
||||
@@ -2147,7 +2150,15 @@ return view.extend({
|
||||
cell.check = c;
|
||||
txt(cell.title, c.title);
|
||||
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);
|
||||
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": {
|
||||
"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": {
|
||||
"file": {
|
||||
"/proc/stat": [ "read" ]
|
||||
@@ -12,8 +12,8 @@
|
||||
"network.device": [ "status" ],
|
||||
"luci-rpc": [ "getHostHints" ],
|
||||
"luci": [ "getSwconfigPortState" ],
|
||||
"luci.vantage": [ "routes", "thermal", "wired", "wireless" ],
|
||||
"iwinfo": [ "assoclist", "info", "survey" ],
|
||||
"luci.vantage": [ "routes", "survey", "thermal", "wired", "wireless" ],
|
||||
"iwinfo": [ "assoclist", "info" ],
|
||||
"hostapd.*": [ "get_clients", "get_status" ],
|
||||
"umdns": [ "hosts" ],
|
||||
"uci": [ "get" ]
|
||||
|
||||
@@ -43,6 +43,16 @@
|
||||
// when a lookup could not be sent. One lookup a family, so
|
||||
// the work does not grow with the routing table. Read group
|
||||
// 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
|
||||
// with name ''. Validated here, whatever the browser did, and
|
||||
// written to /etc/config/vantage only. Write group
|
||||
@@ -60,8 +70,9 @@
|
||||
|
||||
import { cursor } from 'uci';
|
||||
import { connect } from 'ubus';
|
||||
import { lsdir, readfile } from 'fs';
|
||||
import { access, lsdir, readfile } from 'fs';
|
||||
import * as rtnl from 'rtnl';
|
||||
import * as nl80211 from 'nl80211';
|
||||
|
||||
const CONFIG = 'vantage';
|
||||
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_MAX = 65536; // bytes read of it
|
||||
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
|
||||
// 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
|
||||
// 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.
|
||||
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}$/;
|
||||
@@ -683,6 +696,39 @@ function routes() {
|
||||
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
|
||||
function switch_port(v) {
|
||||
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: {
|
||||
args: { mac: 'mac', name: 'name', icon: 'icon' },
|
||||
call: function(request) {
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-theme-vantage
|
||||
PKG_VERSION:=1.0.19
|
||||
PKG_RELEASE:=43
|
||||
PKG_VERSION:=1.0.20
|
||||
PKG_RELEASE:=44
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
LUCI_TITLE:=Vantage theme
|
||||
|
||||
@@ -56,7 +56,7 @@ const MOTION_EVENT = 'vantage:tick';
|
||||
key, also what the dashboard reads under another theme) counts until
|
||||
the router has one. */
|
||||
/* 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
|
||||
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;
|
||||
|
||||
@@ -1939,6 +1939,10 @@ div.right > .btn, div.right > .cbi-button, div.right > button { margin-left: 0;
|
||||
.ifacebadge img { width: 16px; height: 16px; }
|
||||
.ifacebadge.large { padding: 6px 10px; gap: 8px; font-size: var(--fs-base); }
|
||||
.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); }
|
||||
/* 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; }
|
||||
@@ -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 > 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; }
|
||||
/* 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); }
|
||||
.cbi-section > .table + h3, .cbi-section > .table + .cbi-title, .cbi-section > div + h3 { margin-top: var(--sp-6); }
|
||||
.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 */
|
||||
.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 {
|
||||
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 */
|
||||
}
|
||||
.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 > 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; }
|
||||
.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):first-child > .td { border-top: 0; }
|
||||
.cbi-section-table-row:has(> .td[data-name="_stat"] big) > .td { background: var(--s2); }
|
||||
/* 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); }
|
||||
|
||||
/* progress bars (their text only exists in title): a thin track, the
|
||||
|
||||
@@ -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))
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user