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
|
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
@@ -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
|
||||||
|
|||||||
@@ -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,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'
|
||||||
|
|||||||
@@ -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
@@ -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 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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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