diff --git a/htdocs/luci-static/resources/view/homeproxy/client.js b/htdocs/luci-static/resources/view/homeproxy/client.js
index a7a3927f..1d90b3b2 100644
--- a/htdocs/luci-static/resources/view/homeproxy/client.js
+++ b/htdocs/luci-static/resources/view/homeproxy/client.js
@@ -378,22 +378,23 @@ return view.extend({
so = ss.option(form.ListValue, 'node', _('Node'),
_('Outbound node'));
+ so.value('urltest', _('URLTest'));
for (var i in proxy_nodes)
so.value(i, proxy_nodes[i]);
- so.validate = L.bind(hp.validateUniqueValue, this, data[0], 'routing_node', 'node');
so.editable = true;
so = ss.option(form.ListValue, 'domain_strategy', _('Domain strategy'),
_('If set, the server domain name will be resolved to IP before connecting.
'));
for (var i in hp.dns_strategy)
so.value(i, hp.dns_strategy[i]);
+ so.depends({'node': 'urltest', '!reverse': true});
so.modalonly = true;
so = ss.option(widgets.DeviceSelect, 'bind_interface', _('Bind interface'),
_('The network interface to bind to.'));
so.multiple = false;
so.noaliases = true;
- so.depends('outbound', '');
+ so.depends({'outbound': '', 'node': /^((?!urltest$).)+$/});
so.modalonly = true;
so = ss.option(form.ListValue, 'outbound', _('Outbound'),
@@ -416,9 +417,12 @@ return view.extend({
var conflict = false;
uci.sections(data[0], 'routing_node', (res) => {
- if (res['.name'] !== section_id)
+ if (res['.name'] !== section_id) {
if (res.outbound === section_id && res['.name'] == value)
conflict = true;
+ else if (res?.urltest_nodes?.includes(node) && res['.name'] == value)
+ conflict = true;
+ }
});
if (conflict)
return _('Recursive outbound detected!');
@@ -426,6 +430,66 @@ return view.extend({
return true;
}
+ so.depends({'node': 'urltest', '!reverse': true});
+
+ so = ss.option(hp.CBIStaticList, 'urltest_nodes', _('URLTest nodes'),
+ _('List of nodes to test.'));
+ for (var i in proxy_nodes)
+ so.value(i, proxy_nodes[i]);
+ so.depends('node', 'urltest');
+ so.modalonly = true;
+
+ so = ss.option(form.Value, 'urltest_url', _('Test URL'),
+ _('The URL to test. https://www.gstatic.com/generate_204
will be used if empty.'));
+ so.validate = function(section_id, value) {
+ if (section_id && value) {
+ try {
+ var url = new URL(value);
+ if (!url.hostname)
+ return _('Expecting: %s').format(_('valid URL'));
+ }
+ catch(e) {
+ return _('Expecting: %s').format(_('valid URL'));
+ }
+ }
+
+ return true;
+ }
+ so.depends('node', 'urltest');
+ so.modalonly = true;
+
+ so = ss.option(form.Value, 'urltest_interval', _('Test interval'),
+ _('The test interval in seconds. 180
will be used if empty.'));
+ so.datatype = 'uinteger';
+ so.validate = function(section_id, value) {
+ if (section_id && value) {
+ var idle_timeout = this.map.lookupOption('urltest_idle_timeout', section_id)[0].formvalue(section_id) || '1800';
+ if (parseInt(value) > parseInt(idle_timeout))
+ return _('Test interval must be less or equal than idle timeout.');
+ }
+
+ return true;
+ }
+ so.depends('node', 'urltest');
+ so.modalonly = true;
+
+ so = ss.option(form.Value, 'urltest_tolerance', _('Test tolerance'),
+ _('The test tolerance in milliseconds. 50
will be used if empty.'));
+ so.datatype = 'uinteger';
+ so.depends('node', 'urltest');
+ so.modalonly = true;
+
+ so = ss.option(form.Value, 'urltest_idle_timeout', _('Idle timeout'),
+ _('The idle timeout in seconds. 1800
will be used if empty.'));
+ so.datatype = 'uinteger';
+ so.depends('node', 'urltest');
+ so.modalonly = true;
+
+ so = ss.option(form.Flag, 'urltest_interrupt_exist_connections', _('Interrupt existing connections'),
+ _('Interrupt existing connections when the selected outbound has changed.'));
+ so.default = so.disabled;
+ so.depends('node', 'urltest');
+ so.modalonly = true;
/* Routing nodes end */
/* Routing rules start */
@@ -580,13 +644,12 @@ return view.extend({
_('Match user name.'));
so.modalonly = true;
- so = ss.taboption('field_other', form.MultiValue, 'rule_set', _('Rule set'),
+ so = ss.taboption('field_other', hp.CBIStaticList, 'rule_set', _('Rule set'),
_('Match rule set.'));
so.load = function(section_id) {
delete this.keylist;
delete this.vallist;
- this.value('', _('-- Please choose --'));
uci.sections(data[0], 'ruleset', (res) => {
if (res.enabled === '1')
this.value(res['.name'], res.label);
@@ -920,13 +983,12 @@ return view.extend({
_('Match user name.'));
so.modalonly = true;
- so = ss.taboption('field_other', form.MultiValue, 'rule_set', _('Rule set'),
+ so = ss.taboption('field_other', hp.CBIStaticList, 'rule_set', _('Rule set'),
_('Match rule set.'));
so.load = function(section_id) {
delete this.keylist;
delete this.vallist;
- this.value('', _('-- Please choose --'));
uci.sections(data[0], 'ruleset', (res) => {
if (res.enabled === '1')
this.value(res['.name'], res.label);
diff --git a/root/etc/homeproxy/scripts/generate_client.uc b/root/etc/homeproxy/scripts/generate_client.uc
index dce37e7d..3716a0d8 100755
--- a/root/etc/homeproxy/scripts/generate_client.uc
+++ b/root/etc/homeproxy/scripts/generate_client.uc
@@ -300,6 +300,8 @@ function get_outbound(cfg) {
const node = uci.get(uciconfig, cfg, 'node');
if (isEmpty(node))
die(sprintf("%s's node is missing, please check your configuration.", cfg));
+ else if (node === 'urltest')
+ return 'cfg-' + cfg + '-out';
else
return 'cfg-' + node + '-out';
}
@@ -571,17 +573,39 @@ if (!isEmpty(main_node)) {
push(config.outbounds, generate_outbound(main_udp_node_cfg));
config.outbounds[length(config.outbounds)-1].tag = 'main-udp-out';
}
-} else if (!isEmpty(default_outbound))
+} else if (!isEmpty(default_outbound)) {
+ let urltest_nodes = [],
+ routing_nodes = [];
+
uci.foreach(uciconfig, uciroutingnode, (cfg) => {
if (cfg.enabled !== '1')
return;
- const outbound = uci.get_all(uciconfig, cfg.node) || {};
- push(config.outbounds, generate_outbound(outbound));
- config.outbounds[length(config.outbounds)-1].domain_strategy = cfg.domain_strategy;
- config.outbounds[length(config.outbounds)-1].bind_interface = cfg.bind_interface;
- config.outbounds[length(config.outbounds)-1].detour = get_outbound(cfg.outbound);
+ if (cfg.node === 'urltest') {
+ push(config.outbounds, {
+ type: 'urltest',
+ tag: 'cfg-' + cfg['.name'] + '-out',
+ outbounds: map(cfg.urltest_nodes, (k) => `cfg-${k}-out`),
+ url: cfg.urltest_url,
+ interval: cfg.urltest_interval ? (cfg.urltest_interval + 's') : null,
+ tolerance: strToInt(cfg.urltest_tolerance),
+ idle_timeout: cfg.urltest_idle_timeout ? (cfg.urltest_idle_timeout + 's') : null,
+ interrupt_exist_connections: (cfg.urltest_interrupt_exist_connections === '1')
+ });
+ urltest_nodes = [...urltest_nodes, ...filter(cfg.urltest_nodes, ((l) => !~index(urltest_nodes, l)))];
+ } else {
+ const outbound = uci.get_all(uciconfig, cfg.node) || {};
+ push(config.outbounds, generate_outbound(outbound));
+ config.outbounds[length(config.outbounds)-1].domain_strategy = cfg.domain_strategy;
+ config.outbounds[length(config.outbounds)-1].bind_interface = cfg.bind_interface;
+ config.outbounds[length(config.outbounds)-1].detour = get_outbound(cfg.outbound);
+ push(routing_nodes, cfg.node);
+ }
});
+
+ for (let i in filter(urltest_nodes, ((l) => !~index(routing_nodes, l))))
+ push(config.outbounds, generate_outbound(uci.get_all(uciconfig, i)));
+}
/* Outbound end */
/* Routing rules start */
@@ -593,7 +617,6 @@ config.route = {
outbound: 'dns-out'
}
],
- rule_set: [],
auto_detect_interface: isEmpty(default_interface) ? true : null,
default_interface: default_interface
};
@@ -654,6 +677,7 @@ if (!isEmpty(main_node)) {
/* Rule set */
if (routing_mode === 'custom') {
+ config.route.rule_set = [];
uci.foreach(uciconfig, uciruleset, (cfg) => {
if (cfg.enabled !== '1')
return null;