Merge pull request #5174 from TDT-AG/pr/20210702-luci-app-mwan3

luci-app-mwan3: convert to JS
This commit is contained in:
Florian Eckert 2021-07-12 09:15:48 +02:00 committed by GitHub
commit b6ca2947e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1283 additions and 1628 deletions

View file

@ -6,17 +6,12 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for the MWAN3 multiwan hotplug script
LUCI_DEPENDS:=+luci-compat \
+mwan3 \
+libuci-lua \
+luci-mod-admin-full \
+luci-lib-nixio
LUCI_TITLE:=LuCI support for the MWAN3 MultiWAN Manager
LUCI_DEPENDS:=+mwan3
LUCI_PKGARCH:=all
PKG_LICENSE:=GPLv2
PKG_MAINTAINER:=Aedan Renner <chipdankly@gmail.com> \
Florian Eckert <fe@dev.tdt.de>
PKG_MAINTAINER:=Florian Eckert <fe@dev.tdt.de>
include ../../luci.mk

View file

@ -0,0 +1,8 @@
#mwan3-service-status > .alert-message {
display: inline-block;
margin: 1rem;
padding: 1rem;
width: 15rem;
height: 6rem;
vertical-align: middle;
}

View file

@ -0,0 +1,43 @@
'use strict';
'require form';
'require view';
return view.extend({
render: function () {
var m, s, o;
m = new form.Map('mwan3', _('MultiWAN Manager - Globals'));
s = m.section(form.NamedSection, 'globals', 'globals');
o = s.option(form.Value, 'mmx_mask', _('Firewall mask'),
_('Enter value in hex, starting with <code>0x</code>'));
o.datatype = 'hex(4)';
o.default = '0x3F00';
o = s.option(form.Flag, 'logging', _('Logging'),
_('Enables global firewall logging'));
o = s.option(form.ListValue, 'loglevel', _('Loglevel'),
_('Firewall loglevel'));
o.default = 'notice';
o.value('emerg', _('Emergency'));
o.value('alert', _('Alert'));
o.value('crit', _('Critical'));
o.value('error', _('Error'));
o.value('warning', _('Warning'));
o.value('notice', _('Notice'));
o.value('info', _('Info'));
o.value('debug', _('Debug'));
o.depends('logging', '1');
o = s.option(form.DynamicList, 'rt_table_lookup',
_('Routing table lookup'),
_('Also scan this Routing table for connected networks'));
o.datatype = 'uinteger';
o.value('220', _('Routing table %d').format('220'));
return m.render();
}
})

View file

@ -0,0 +1,276 @@
'use strict';
'require form';
'require fs';
'require view';
'require uci';
return view.extend({
load: function() {
return Promise.all([
L.resolveDefault(fs.stat('/usr/bin/httping'), {}),
L.resolveDefault(fs.stat('/usr/bin/nping'), {}),
L.resolveDefault(fs.stat('/usr/bin/arping'), {}),
uci.load('network')
]);
},
render: function (stats) {
var m, s, o;
m = new form.Map('mwan3', _('MultiWAN Manager - Interfaces'),
_('Mwan3 requires that all interfaces have a unique metric configured in /etc/config/network.') + '<br />' +
_('Names must match the interface name found in /etc/config/network.') + '<br />' +
_('Names may contain characters A-Z, a-z, 0-9, _ and no spaces-') + '<br />' +
_('Interfaces may not share the same name as configured members, policies or rules.'));
s = m.section(form.GridSection, 'interface');
s.addremove = true;
s.anonymous = false;
s.nodescriptions = true;
o = s.option(form.Flag, 'enabled', _('Enabled'));
o.default = false;
o = s.option(form.ListValue, 'initial_state', _('Initial state'),
_('Expect interface state on up event'));
o.default = 'online';
o.value('online', _('Online'));
o.value('offline', _('Offline'));
o.modalonly = true;
o = s.option(form.ListValue, 'family', _('Internet Protocol'));
o.default = 'ipv4';
o.value('ipv4', _('IPv4'));
o.value('ipv6', _('IPv6'));
o.modalonly = true;
o = s.option(form.DynamicList, 'track_ip', _('Tracking hostname or IP address'),
_('This hostname or IP address will be pinged to determine if the link is up or down. Leave blank to assume interface is always online'));
o.datatype = 'host';
o.modalonly = true;
o = s.option(form.ListValue, 'track_method', _('Tracking method'));
o.default = 'ping';
o.value('ping');
if (stats[0].type === 'file') {
o.value('httping');
}
if (stats[1].type === 'file') {
o.value('nping-tcp');
o.value('nping-udp');
o.value('nping-icmp');
o.value('nping-arp');
}
if (stats[2].type === 'file') {
o.value('arping');
}
o = s.option(form.Flag, 'httping_ssl', _('Enable ssl tracking'),
_('Enables https tracking on ssl port 443'));
o.depends('track_method', 'httping');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'reliability', _('Tracking reliability'),
_('Acceptable values: 1-100. This many Tracking IP addresses must respond for the link to be deemed up'));
o.datatype = 'range(1, 100)';
o.default = '1';
o = s.option(form.ListValue, 'count', _('Ping count'));
o.default = '1';
o.value('1');
o.value('2');
o.value('3');
o.value('4');
o.value('5');
o.modalonly = true;
o = s.option(form.Value, 'size', _('Ping size'));
o.default = '56';
o.depends('track_method', 'ping');
o.value('8');
o.value('24');
o.value('56');
o.value('120');
o.value('248');
o.value('504');
o.value('1016');
o.value('1472');
o.value('2040');
o.datatype = 'range(1, 65507)';
o.modalonly = true;
o =s.option(form.Value, 'max_ttl', _('Max TTL'));
o.default = '60';
o.depends('track_method', 'ping');
o.value('10');
o.value('20');
o.value('30');
o.value('40');
o.value('50');
o.value('60');
o.value('70');
o.datatype = 'range(1, 255)';
o.modalonly = true;
o = s.option(form.Flag, 'check_quality', _('Check link quality'));
o.depends('track_method', 'ping');
o.default = false;
o.modalonly = true;
o = s.option(form.Value, 'failure_latency', _('Failure latency [ms]'));
o.depends('check_quality', '1');
o.default = '1000';
o.value('25');
o.value('50');
o.value('75');
o.value('100');
o.value('150');
o.value('200');
o.value('250');
o.value('300');
o.modalonly = true;
o = s.option(form.Value, 'failure_loss', _('Failure packet loss [%]'));
o.depends('check_quality', '1');
o.default = '40';
o.value('2');
o.value('5');
o.value('10');
o.value('20');
o.value('25');
o.modalonly = true;
o = s.option(form.Value, 'recovery_latency', _('Recovery latency [ms]'));
o.depends('check_quality', '1');
o.default = '500';
o.value('25');
o.value('50');
o.value('75');
o.value('100');
o.value('150');
o.value('200');
o.value('250');
o.value('300');
o.modalonly = true;
o = s.option(form.Value, 'recovery_loss', _('Recovery packet loss [%]'));
o.depends('check_quality', '1');
o.default = '10';
o.value('2');
o.value('5');
o.value('10');
o.value('20');
o.value('25');
o.modalonly = true;
o = s.option(form.ListValue, "timeout", _("Ping timeout"));
o.default = '4';
for (var i = 1; i <= 10; i++)
o.value(String(i), N_(i, '%d second', '%d seconds').format(i));
o.modalonly = true;
o = s.option(form.ListValue, 'interval', _('Ping interval'));
o.default = '10';
o.value('1', _('%d second').format('1'));
o.value('3', _('%d seconds').format('3'));
o.value('5', _('%d seconds').format('5'));
o.value('10', _('%d seconds').format('10'));
o.value('20', _('%d seconds').format('20'));
o.value('30', _('%d seconds').format('30'));
o.value('60', _('%d minute').format('1'));
o.value('300', _('%d minutes').format('5'));
o.value('600', _('%d minutes').format('10'));
o.value('900', _('%d minutes').format('15'));
o.value('1800', _('%d minutes').format('30'));
o.value('3600', _('%d hour').format('1'));
o = s.option(form.Value, 'failure_interval', _('Failure interval'),
_('Ping interval during failure detection'));
o.default = '5';
o.value('1', _('%d second').format('1'));
o.value('3', _('%d seconds').format('3'));
o.value('5', _('%d seconds').format('5'));
o.value('10', _('%d seconds').format('10'));
o.value('20', _('%d seconds').format('20'));
o.value('30', _('%d seconds').format('30'));
o.value('60', _('%d minute').format('1'));
o.value('300', _('%d minutes').format('5'));
o.value('600', _('%d minutes').format('10'));
o.value('900', _('%d minutes').format('15'));
o.value('1800', _('%d minutes').format('30'));
o.value('3600', _('%d hour').format('1'));
o.modalonly = true;
o = s.option(form.Flag, 'keep_failure_interval', _('Keep failure interval'),
_('Keep ping failure interval during failure state'));
o.default = false;
o.modalonly = true;
o = s.option(form.Value, 'recovery_interval', _('Recovery interval'),
_('Ping interval during failure recovering'));
o.default = '5';
o.value('1', _('%d second').format('1'));
o.value('3', _('%d seconds').format('3'));
o.value('5', _('%d seconds').format('5'));
o.value('10', _('%d seconds').format('10'));
o.value('20', _('%d seconds').format('20'));
o.value('30', _('%d seconds').format('30'));
o.value('60', _('%d minute').format('1'));
o.value('300', _('%d minutes').format('5'));
o.value('600', _('%d minutes').format('10'));
o.value('900', _('%d minutes').format('15'));
o.value('1800', _('%d minutes').format('30'));
o.value('3600', _('%d hour').format('1'));
o.modalonly = true;
o = s.option(form.ListValue, 'down', _('Interface down'),
_('Interface will be deemed down after this many failed ping tests'));
o.default = '5';
o.value('1');
o.value('2');
o.value('3');
o.value('4');
o.value('5');
o.value('6');
o.value('7');
o.value('8');
o.value('9');
o.value('10');
o = s.option(form.ListValue, 'up', _('Interface up'),
_('Downed interface will be deemed up after this many successful ping tests'));
o.default = "5";
o.value('1');
o.value('2');
o.value('3');
o.value('4');
o.value('5');
o.value('6');
o.value('7');
o.value('8');
o.value('9');
o.value('10');
o = s.option(form.ListValue, 'flush_conntrack', _('Flush conntrack table'),
_('Flush global firewall conntrack table on interface events'));
o.value('ifup', _('ifup (netifd)'));
o.value('ifdown', _('ifdown (netifd)'));
o.value('connected', _('connected (mwan3)'));
o.value('disconnected', _('disconnected (mwan3)'));
o.modalonly = true;
o = s.option(form.DummyValue, 'metric', _('Metric'),
_('This displays the metric assigned to this interface in /etc/config/network'));
o.rawhtml = true;
o.cfgvalue = function(s) {
var metric = uci.get('network', s, 'metric')
if (metric)
return metric;
else
return _('No interface metric set!');
}
return m.render();
}
})

View file

@ -0,0 +1,43 @@
'use strict';
'require form';
'require view';
'require uci';
return view.extend({
load: function() {
return Promise.all([
uci.load('mwan3')
]);
},
render: function () {
var m, s, o;
m = new form.Map('mwan3', _('MultiWAN Manager - Members'),
_('Members are profiles attaching a metric and weight to an MWAN interface.') + '<br />' +
_('Names may contain characters A-Z, a-z, 0-9, _ and no spaces.') + '<br />' +
_('Members may not share the same name as configured interfaces, policies or rules.'));
s = m.section(form.GridSection, 'member');
s.addremove = true;
s.anonymous = false;
s.nodescriptions = true;
o = s.option(form.ListValue, 'interface', _('Interface'));
var options = uci.sections('mwan3', 'interface')
for (var i = 0; i < options.length; i++) {
var value = options[i]['.name'];
o.value(value);
}
o = s.option(form.Value, 'metric', _('Metric'),
_('Acceptable values: 1-256. Defaults to 1 if not set'));
o.datatype = 'range(1, 256)';
o = s.option(form.Value, 'weight', ('Weight'),
_('Acceptable values: 1-1000. Defaults to 1 if not set'));
o.datatype = 'range(1, 1000)';
return m.render();
}
})

View file

@ -0,0 +1,52 @@
'use strict';
'require view';
'require fs';
'require ui';
var isReadonlyView = !L.hasViewPermission() || null;
return view.extend({
load: function() {
return L.resolveDefault(fs.read('/etc/mwan3.user'), '');
},
handleSave: function(ev) {
var value = (document.querySelector('textarea').value || '').trim().replace(/\r\n/g, '\n') + '\n';
return fs.write('/etc/mwan3.user', value).then(function(rc) {
document.querySelector('textarea').value = value;
ui.addNotification(null, E('p', _('Contents have been saved.')), 'info');
}).catch(function(e) {
ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message)));
});
},
render: function(mwan3user) {
return E([
E('h2', _('MultiWAN Manager - Notify')),
E('p', { 'class': 'cbi-section-descr' },
_('This section allows you to modify the content of \"/etc/mwan3.user\".') + '<br/>' +
_('The file is also preserved during sysupgrade.') + '<br/>' +
'<br />' +
_('Notes:') + '<br />' +
_('This file is interpreted as a shell script.') + '<br />' +
_('The first line of the script must be &#34;#!/bin/sh&#34; without quotes.') + '<br />' +
_('Lines beginning with # are comments and are not executed.') + '<br />' +
_('Put your custom mwan3 action here, they will be executed with each netifd hotplug interface event on interfaces for which mwan3 is enabled.') + '<br />' +
'<br />' +
_('There are three main environment variables that are passed to this script.') + '<br />' +
'<br />' +
_('%s: Name of the action that triggered this event').format('$ACTION') + '<br />' +
_('* %s: Is called by netifd and mwan3track').format('ifup') + '<br />' +
_('* %s: Is called by netifd and mwan3track').format('ifdown') + '<br />' +
_('* %s: Is only called by mwan3track if tracking was successful').format('connected') + '<br />' +
_('* %s: Is only called by mwan3track if tracking has failed').format('disonnected') + '<br />' +
_('%s: Name of the interface which went up or down (e.g. \"wan\" or \"wwan\")').format('$INTERFACE') + '<br />' +
_('%s: Name of Physical device which interface went up or down (e.g. \"eth0\" or \"wwan0\")').format('$DEVICE') + '<br />'),
E('p', {}, E('textarea', { 'style': 'width:100%', 'rows': 10, 'disabled': isReadonlyView }, [ mwan3user != null ? mwan3user : '' ]))
]);
},
handleSaveApply: null,
handleReset: null
});

View file

@ -0,0 +1,46 @@
'use strict';
'require form';
'require view';
'require uci';
return view.extend({
load: function() {
return Promise.all([
uci.load('mwan3')
]);
},
render: function () {
var m, s, o;
m = new form.Map('mwan3', _('MultiWAN Manager - Policies'),
_('Policies are profiles grouping one or more members controlling how Mwan3 distributes traffic.') +
_('Member interfaces with lower metrics are used first.') +
_('Member interfaces with the same metric will be load-balanced.') +
_('Load-balanced member interfaces distribute more traffic out those with higher weights.') +
_('Names may contain characters A-Z, a-z, 0-9, _ and no spaces.') +
_('Names must be 15 characters or less.') +
_('Policies may not share the same name as configured interfaces, members or rules'));
s = m.section(form.GridSection, 'policy');
s.addremove = true;
s.anonymous = false;
s.nodescriptions = true;
o = s.option(form.DynamicList, 'use_member', _('Member used'));
var options = uci.sections('mwan3', 'member')
for (var i = 0; i < options.length; i++) {
var value = options[i]['.name'];
o.value(value);
}
o = s.option(form.ListValue, 'last_resort', _('Last resort'),
_('When all policy members are offline use this behavior for matched traffic'));
o.default = 'unreachable';
o.value('unreachable', _('unreachable (reject)'));
o.value('blackhole', _('blackhole (drop)'));
o.value('default', _('default (use main routing table)'));
return m.render();
}
})

View file

@ -0,0 +1,107 @@
'use strict';
'require form';
'require fs';
'require view';
'require uci';
return view.extend({
load: function() {
return Promise.all([
fs.exec_direct('/usr/libexec/luci-mwan3', ['ipset', 'dump']),
uci.load('mwan3')
]);
},
render: function (data) {
var m, s, o;
m = new form.Map('mwan3', _('MultiWAN Manager - Rules'),
_('Rules specify which traffic will use a particular MWAN policy.') + '<br />' +
_('Rules are based on IP address, port or protocol.') + '<br />' +
_('Rules are matched from top to bottom.') + '<br />' +
_('Rules below a matching rule are ignored.') + '<br />' +
_('Traffic not matching any rule is routed using the main routing table.') + '<br />' +
_('Traffic destined for known (other than default) networks is handled by the main routing table.') + '<br />' +
_('Traffic matching a rule, but all WAN interfaces for that policy are down will be blackholed.') + '<br />' +
_('Names may contain characters A-Z, a-z, 0-9, _ and no spaces.') + '<br />' +
_('Rules may not share the same name as configured interfaces, members or policies.'));
s = m.section(form.GridSection, 'rule');
s.addremove = true;
s.anonymous = false;
s.nodescriptions = true;
o = s.option(form.ListValue, 'family', _('Internet Protocol'));
o.default = '';
o.value('', _('IPv4 and IPv6'));
o.value('ipv4', _('IPv4 only'));
o.value('ipv6', _('IPv6 only'));
o.modalonly = true;
o = s.option(form.Value, 'src_ip', _('Source address'),
_('Supports CIDR notation (eg \"192.168.100.0/24\") without quotes'));
o.datatype = 'ipaddr';
o = s.option(form.Value, 'src_port', _('Source port'),
_('May be entered as a single or multiple port(s) (eg \"22\" or \"80,443\") or as a portrange (eg \"1024:2048\") without quotes'));
o.depends('proto', 'tcp');
o.depends('proto', 'udp');
o = s.option(form.Value, 'dest_ip', _('Destination address'),
_('Supports CIDR notation (eg \"192.168.100.0/24\") without quotes'));
o.datatype = 'ipaddr';
o = s.option(form.Value, 'dest_port', _('Destination port'),
_('May be entered as a single or multiple port(s) (eg \"22\" or \"80,443\") or as a portrange (eg \"1024:2048\") without quotes'));
o.depends('proto', 'tcp');
o.depends('proto', 'udp');
o = s.option(form.Value, 'proto', _('Protocol'),
_('View the content of /etc/protocols for protocol description'));
o.default = 'all';
o.rmempty = false;
o.value('all');
o.value('tcp');
o.value('udp');
o.value('icmp');
o.value('esp');
o = s.option(form.ListValue, 'sticky', _('Sticky'),
_('Traffic from the same source IP address that previously matched this rule within the sticky timeout period will use the same WAN interface'));
o.default = '0';
o.value('1', _('Yes'));
o.value('0', _('No'));
o.modalonly = true;
o = s.option(form.Value, 'timeout', _('Sticky timeout'),
_('Seconds. Acceptable values: 1-1000000. Defaults to 600 if not set'));
o.datatype = 'range(1, 1000000)';
o.modalonly = true;
o = s.option(form.Value, 'ipset', _('IPset'),
_('Name of IPset rule. Requires IPset rule in /etc/dnsmasq.conf (eg \"ipset=/youtube.com/youtube\")'));
o.value('', _('-- Please choose --'));
var ipsets = data[0].split(/\n/);
for (var i = 0; i < ipsets.length; i++) {
if (ipsets[i].length > 0)
o.value(ipsets[i]);
}
o.modalonly = true;
o = s.option(form.Flag, 'logging', _('Logging'),
_('Enables firewall rule logging (global mwan3 logging must also be enabled)'));
o.modalonly = true;
o = s.option(form.ListValue, 'use_policy', _('Policy assigned'));
var options = uci.sections('mwan3', 'policy')
for (var i = 0; i < options.length; i++) {
var value = options[i]['.name'];
o.value(value);
}
o.value('unreachable', _('unreachable (reject)'));
o.value('blackhole', _('blackhole (drop)'));
o.value('default', _('default (use main routing table)'));
return m.render();
}
})

View file

@ -0,0 +1,22 @@
'use strict';
'require fs';
'require view';
return view.extend({
load: function() {
return L.resolveDefault(fs.exec_direct('/usr/sbin/mwan3', [ 'status' ]),'');
},
render: function (report) {
return E('div', { 'class': 'cbi-map', 'id': 'map' }, [
E('h2', _('MultiWAN Manager - Status')),
E('div', { 'class': 'cbi-section' }, [
E('pre', [ report ])
]),
])
},
handleSaveApply: null,
handleSave: null,
handleReset: null
})

View file

@ -0,0 +1,116 @@
'use strict';
'require fs';
'require uci';
'require dom';
'require ui';
'require view';
return view.extend({
handleCommand: function(exec, args) {
var buttons = document.querySelectorAll('.cbi-button');
for (var i = 0; i < buttons.length; i++)
buttons[i].setAttribute('disabled', 'true');
return fs.exec(exec, args).then(function(res) {
var out = document.querySelector('.command-output');
out.style.display = '';
dom.content(out, [ res.stdout || '', res.stderr || '' ]);
}).catch(function(err) {
ui.addNotification(null, E('p', [ err ]))
}).finally(function() {
for (var i = 0; i < buttons.length; i++)
buttons[i].removeAttribute('disabled');
});
},
handleAction: function(ev) {
var iface = document.getElementById('iface');
var task = document.getElementById('task');
switch (task.value) {
case 'gateway':
return this.handleCommand('/usr/libexec/luci-mwan3',
[ 'diag', 'gateway', iface.value ]);
case 'tracking':
return this.handleCommand('/usr/libexec/luci-mwan3',
[ 'diag', 'tracking', iface.value ]);
case 'rules':
return this.handleCommand('/usr/libexec/luci-mwan3',
[ 'diag', 'rules', iface.value ]);
case 'routes':
return this.handleCommand('/usr/libexec/luci-mwan3',
[ 'diag', 'routes', iface.value ]);
case 'ifup':
return this.handleCommand('/usr/sbin/mwan3',
[ 'ifup', iface.value]);
case 'ifdown':
return this.handleCommand('/usr/sbin/mwan3',
[ 'ifdown', iface.value]);
}
},
load: function() {
return Promise.all([
uci.load('mwan3')
]);
},
render: function () {
var taskSel = [
E('option', { 'value': 'gateway' }, [ _('Ping default gateway') ]),
E('option', { 'value': 'tracking' }, [ _('Ping tracking IP') ]),
E('option', { 'value': 'rules' }, [ _('Check IP rules') ]),
E('option', { 'value': 'routes' }, [ _('Check routing table') ]),
E('option', { 'value': 'ifup' }, [ _('Hotplug ifup') ]),
E('option', { 'value': 'ifdown' }, [ _('Hotplug ifdown') ])
];
var ifaceSel = [E('option', { value: '' }, [_('-- Interface Selection --')])];
var options = uci.sections('mwan3', 'interface')
for (var i = 0; i < options.length; i++) {
ifaceSel.push(E('option', { 'value': options[i]['.name'] }, options[i]['.name']));
}
return E('div', { 'class': 'cbi-map', 'id': 'map' }, [
E('h2', {}, [ _('MultiWAN Manager - Diagnostics') ]),
E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-section-node' }, [
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, [ _('Interface') ]),
E('div', { 'class': 'cbi-value-field' }, [
E('select', {'class': 'cbi-input-select', 'id': 'iface'},
ifaceSel
)
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, [ _('Task') ]),
E('div', { 'class': 'cbi-value-field' }, [
E('select', { 'class': 'cbi-input-select', 'id': 'task' },
taskSel
)
])
])
])
]),
'\xa0',
E('pre', { 'class': 'command-output', 'style': 'display:none' }),
'\xa0',
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button cbi-button-apply',
'id': 'execute',
'click': ui.createHandlerFn(this, 'handleAction')
}, [ _('Execute') ]),
]),
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
})

View file

@ -0,0 +1,104 @@
'use strict';
'require poll';
'require view';
'require rpc';
var callMwan3Status = rpc.declare({
object: 'mwan3',
method: 'status',
expect: { },
});
document.querySelector('head').appendChild(E('link', {
'rel': 'stylesheet',
'type': 'text/css',
'href': L.resource('view/mwan3/mwan3.css')
}));
function renderMwan3Status(status) {
if (!status.interfaces)
return '<strong>%h</strong>'.format(_('No MWAN interfaces found'));
var statusview = '';
for ( var iface in status.interfaces) {
var state = '';
var css = '';
var time = '';
var tname = '';
switch (status.interfaces[iface].status) {
case 'online':
state = _('Online');
css = 'success';
time = '%t'.format(status.interfaces[iface].online);
tname = _('Uptime');
css = 'success';
break;
case 'offline':
state = _('Offline');
css = 'danger';
time = '%t'.format(status.interfaces[iface].offline);
tname = _('Downtime');
break;
case 'notracking':
state = _('No Tracking');
if ((status.interfaces[iface].uptime) > 0) {
css = 'success';
time = '%t'.format(status.interfaces[iface].uptime);
tname = _('Uptime');
}
else {
css = 'warning';
time = '';
tname = '';
}
break;
default:
state = _('Disabled');
css = 'warning';
time = '';
tname = '';
break;
}
statusview += '<div class="alert-message %h">'.format(css);
statusview += '<div><strong>%h:&nbsp;</strong>%h</div>'.format(_('Interface'), iface);
statusview += '<div><strong>%h:&nbsp;</strong>%h</div>'.format(_('Status'), state);
if (time)
statusview += '<div><strong>%h:&nbsp;</strong>%h</div>'.format(tname, time);
statusview += '</div>';
}
return statusview;
}
return view.extend({
load: function() {
return Promise.all([
callMwan3Status(),
]);
},
render: function (data) {
poll.add(function() {
return callMwan3Status().then(function(result) {
var view = document.getElementById('mwan3-service-status');
view.innerHTML = renderMwan3Status(result);
});
});
return E('div', { class: 'cbi-map' }, [
E('h2', [ _('MultiWAN Manager - Overview') ]),
E('div', { class: 'cbi-section' }, [
E('div', { 'id': 'mwan3-service-status' }, [
E('em', { 'class': 'spinning' }, [ _('Collecting data ...') ])
])
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
})

View file

@ -0,0 +1,22 @@
'use strict';
'require fs';
'require view';
return view.extend({
load: function() {
return L.resolveDefault(fs.exec_direct('/usr/sbin/mwan3', [ 'internal', 'ipv4' ]),'');
},
render: function (report) {
return E('div', { 'class': 'cbi-map', 'id': 'map' }, [
E('h2', _('MultiWAN Manager - Troubleshooting')),
E('div', { 'class': 'cbi-section' }, [
E('pre', [ report ])
]),
])
},
handleSaveApply: null,
handleSave: null,
handleReset: null
})

View file

@ -0,0 +1,117 @@
'use strict';
'require baseclass';
'require rpc';
var callMwan3Status = rpc.declare({
object: 'mwan3',
method: 'status',
expect: { },
});
document.querySelector('head').appendChild(E('link', {
'rel': 'stylesheet',
'type': 'text/css',
'href': L.resource('view/mwan3/mwan3.css')
}));
return baseclass.extend({
title: _('MultiWAN Manager'),
load: function() {
return Promise.all([
callMwan3Status(),
]);
},
render: function (result) {
if (!result[0].interfaces)
return null;
var container = E('div', { 'id': 'mwan3-service-status' });
var iface;
for ( iface in result[0].interfaces) {
var state = '';
var css = '';
var time = '';
var tname = '';
switch (result[0].interfaces[iface].status) {
case 'online':
state = _('Online');
css = 'alert-message success';
time = '%t'.format(result[0].interfaces[iface].online);
tname = _('Uptime');
break;
case 'offline':
state = _('Offline');
css = 'alert-message danger';
time = '%t'.format(result[0].interfaces[iface].offline);
tname = _('Downtime');
break;
case 'notracking':
state = _('No Tracking');
if ((result[0].interfaces[iface].uptime) > 0) {
css = 'alert-message success';
time = '%t'.format(result[0].interfaces[iface].uptime);
tname = _('Uptime');
}
else {
css = 'alert-message warning';
time = '';
tname = '';
}
break;
default:
css = 'alert-message warning';
state = _('Disabled');
time = '';
tname = '';
break;
}
if (time !== '' ) {
container.appendChild(
E('div', { 'class': css }, [
E('div', {}, [
E('strong', {}, [
_('Interface'), ':', ' '
]),
iface
]),
E('div', {}, [
E('strong', {}, [
_('Status'), ':', ' '
]),
state
]),
E('div', {}, [
E('strong', {}, [
tname, ':', ' '
]),
time
])
])
);
}
else {
container.appendChild(
E('div', { 'class': css }, [
E('div', {}, [
E('strong', {}, [
_('Interface'), ':', ' '
]),
iface
]),
E('div', {}, [
E('strong', {}, [
_('Status'), ':', ' '
]),
state
])
])
);
}
}
return container;
}
});

View file

@ -1,320 +0,0 @@
-- Copyright 2014 Aedan Renner <chipdankly@gmail.com>
-- Copyright 2018 Florian Eckert <fe@dev.tdt.de>
-- Licensed to the public under the GNU General Public License v2.
module("luci.controller.mwan3", package.seeall)
sys = require "luci.sys"
ut = require "luci.util"
ip = "ip -4 "
function index()
if not nixio.fs.access("/etc/config/mwan3") then
return
end
entry({"admin", "status", "mwan"},
alias("admin", "status", "mwan", "overview"),
_("Load Balancing"), 600).acl_depends = { "luci-app-mwan3" }
entry({"admin", "status", "mwan", "overview"},
template("mwan/status_interface"))
entry({"admin", "status", "mwan", "detail"},
template("mwan/status_detail"))
entry({"admin", "status", "mwan", "diagnostics"},
template("mwan/status_diagnostics"))
entry({"admin", "status", "mwan", "troubleshooting"},
template("mwan/status_troubleshooting"))
entry({"admin", "status", "mwan", "interface_status"},
call("mwan_Status"))
entry({"admin", "status", "mwan", "detailed_status"},
call("detailedStatus"))
entry({"admin", "status", "mwan", "diagnostics_display"},
call("diagnosticsData"), nil).leaf = true
entry({"admin", "status", "mwan", "troubleshooting_display"},
call("troubleshootingData"))
entry({"admin", "network", "mwan"},
alias("admin", "network", "mwan", "interface"),
_("Load Balancing"), 600).acl_depends = { "luci-app-mwan3" }
entry({"admin", "network", "mwan", "globals"},
cbi("mwan/globalsconfig"),
_("Globals"), 5).leaf = true
entry({"admin", "network", "mwan", "interface"},
arcombine(cbi("mwan/interface"), cbi("mwan/interfaceconfig")),
_("Interfaces"), 10).leaf = true
entry({"admin", "network", "mwan", "member"},
arcombine(cbi("mwan/member"), cbi("mwan/memberconfig")),
_("Members"), 20).leaf = true
entry({"admin", "network", "mwan", "policy"},
arcombine(cbi("mwan/policy"), cbi("mwan/policyconfig")),
_("Policies"), 30).leaf = true
entry({"admin", "network", "mwan", "rule"},
arcombine(cbi("mwan/rule"), cbi("mwan/ruleconfig")),
_("Rules"), 40).leaf = true
entry({"admin", "network", "mwan", "notify"},
form("mwan/notify"),
_("Notification"), 50).leaf = true
end
function mwan_Status()
local status = ut.ubus("mwan3", "status", {})
luci.http.prepare_content("application/json")
if status ~= nil then
luci.http.write_json(status)
else
luci.http.write_json({})
end
end
function detailedStatus()
local statusInfo = ut.trim(sys.exec("/usr/sbin/mwan3 status"))
luci.http.prepare_content("text/plain")
if statusInfo ~= "" then
luci.http.write(statusInfo)
else
luci.http.write("Unable to get status information")
end
end
function diagnosticsData(interface, task)
function getInterfaceNumber(interface)
local number = 0
local interfaceNumber
local uci = require "luci.model.uci".cursor()
uci:foreach("mwan3", "interface",
function (section)
number = number+1
if section[".name"] == interface then
interfaceNumber = number
end
end
)
return interfaceNumber
end
function diag_command(cmd, device, addr)
if addr and addr:match("^[a-zA-Z0-9%-%.:_]+$") then
local util = io.popen(cmd %{ut.shellquote(device), ut.shellquote(addr)})
if util then
luci.http.write("Command:\n")
luci.http.write(cmd %{ut.shellquote(device),
ut.shellquote(addr)} .. "\n\n")
luci.http.write("Result:\n")
while true do
local ln = util:read("*l")
if not ln then break end
luci.http.write(ln)
luci.http.write("\n")
end
util:close()
end
return
end
end
function get_gateway(interface)
local gateway = nil
local dump = nil
dump = require("luci.util").ubus("network.interface.%s_4" % interface, "status", {})
if not dump then
dump = require("luci.util").ubus("network.interface.%s" % interface, "status", {})
end
if dump and dump.route then
local _, route
for _, route in ipairs(dump.route) do
if dump.route[_].target == "0.0.0.0" then
gateway = dump.route[_].nexthop
end
end
end
return gateway
end
local mArray = {}
local results = ""
local number = getInterfaceNumber(interface)
local uci = require "luci.model.uci".cursor(nil, "/var/state")
local nw = require "luci.model.network".init()
local i18n = require "luci.i18n"
local network = nw:get_network(interface)
local device = network and network:get_interface()
device = device:name()
luci.http.prepare_content("text/plain")
if device then
if task == "ping_gateway" then
local gateway = get_gateway(interface)
if gateway ~= nil then
diag_command("ping -I %s -c 5 -W 1 %s 2>&1", device, gateway)
else
luci.http.prepare_content("text/plain")
luci.http.write(i18n.translatef("No gateway for interface %s found.", interface))
end
elseif task == "ping_trackips" then
local trackips = uci:get("mwan3", interface, "track_ip")
if #trackips > 0 then
for i in pairs(trackips) do
diag_command("ping -I %s -c 5 -W 1 %s 2>&1", device, trackips[i])
end
else
luci.http.write(i18n.translatef("No tracking Hosts for interface %s defined.", interface))
end
elseif task == "check_rules" then
local number = getInterfaceNumber(interface)
local iif = 1000 + number
local fwmark = 2000 + number
local iif_rule = sys.exec(string.format("ip rule | grep %d", iif))
local fwmark_rule = sys.exec(string.format("ip rule | grep %d", fwmark))
if iif_rule ~= "" and fwmark_rule ~= "" then
luci.http.write(i18n.translatef("All required IP rules for interface %s found", interface))
luci.http.write("\n")
luci.http.write(fwmark_rule)
luci.http.write(iif_rule)
elseif iif_rule == "" and fwmark_rule ~= "" then
luci.http.write(i18n.translatef("Only one IP rules for interface %s found", interface))
luci.http.write("\n")
luci.http.write(fwmark_rule)
elseif iif_rule ~= "" and fwmark_rule == "" then
luci.http.write(i18n.translatef("Only one IP rules for interface %s found", interface))
luci.http.write("\n")
luci.http.write(iif_rule)
else
luci.http.write(i18n.translatef("Missing both IP rules for interface %s", interface))
end
elseif task == "check_routes" then
local number = getInterfaceNumber(interface)
local routeTable = sys.exec(string.format("ip route list table %s", number))
if routeTable ~= "" then
luci.http.write(i18n.translatef("Routing table %s for interface %s found", number, interface))
luci.http.write("\n")
luci.http.write(routeTable)
else
luci.http.write(i18n.translatef("Routing table %s for interface %s not found", number, interface))
end
elseif task == "hotplug_ifup" then
os.execute(string.format("/usr/sbin/mwan3 ifup %s", ut.shellquote(interface)))
luci.http.write(string.format("Hotplug ifup sent to interface %s", interface))
elseif task == "hotplug_ifdown" then
os.execute(string.format("/usr/sbin/mwan3 ifdown %s", ut.shellquote(interface)))
luci.http.write(string.format("Hotplug ifdown sent to interface %s", interface))
else
luci.http.write("Unknown task")
end
else
luci.http.write(string.format("Unable to perform diagnostic tests on %s.", interface))
luci.http.write("\n")
luci.http.write("There is no physical or virtual device associated with this interface.")
end
end
function troubleshootingData()
local ver = require "luci.version"
local dash = "-------------------------------------------------"
luci.http.prepare_content("text/plain")
luci.http.write("\n")
luci.http.write("\n")
luci.http.write("Software-Version")
luci.http.write("\n")
luci.http.write(dash)
luci.http.write("\n")
if ver.distversion then
luci.http.write(string.format("OpenWrt - %s", ver.distversion))
luci.http.write("\n")
else
luci.http.write("OpenWrt - unknown")
luci.http.write("\n")
end
if ver.luciversion then
luci.http.write(string.format("LuCI - %s", ver.luciversion))
luci.http.write("\n")
else
luci.http.write("LuCI - unknown")
luci.http.write("\n")
end
luci.http.write("\n")
luci.http.write("\n")
local output = ut.trim(sys.exec("ip a show"))
luci.http.write("Output of \"ip a show\"")
luci.http.write("\n")
luci.http.write(dash)
luci.http.write("\n")
if output ~= "" then
luci.http.write(output)
luci.http.write("\n")
else
luci.http.write("No data found")
luci.http.write("\n")
end
luci.http.write("\n")
luci.http.write("\n")
local output = ut.trim(sys.exec("ip route show"))
luci.http.write("Output of \"ip route show\"")
luci.http.write("\n")
luci.http.write(dash)
luci.http.write("\n")
if output ~= "" then
luci.http.write(output)
luci.http.write("\n")
else
luci.http.write("No data found")
luci.http.write("\n")
end
luci.http.write("\n")
luci.http.write("\n")
local output = ut.trim(sys.exec("ip rule show"))
luci.http.write("Output of \"ip rule show\"")
luci.http.write("\n")
luci.http.write(dash)
luci.http.write("\n")
if output ~= "" then
luci.http.write(output)
luci.http.write("\n")
else
luci.http.write("No data found")
luci.http.write("\n")
end
luci.http.write("\n")
luci.http.write("\n")
luci.http.write("Output of \"ip route list table 1-250\"")
luci.http.write("\n")
luci.http.write(dash)
luci.http.write("\n")
for i=1,250 do
local output = ut.trim(sys.exec(string.format("ip route list table %d", i)))
if output ~= "" then
luci.http.write(string.format("Table %s: ", i))
luci.http.write(output)
luci.http.write("\n")
end
end
luci.http.write("\n")
luci.http.write("\n")
local output = ut.trim(sys.exec("iptables -L -t mangle -v -n"))
luci.http.write("Output of \"iptables -L -t mangle -v -n\"")
luci.http.write("\n")
luci.http.write(dash)
luci.http.write("\n")
if output ~= "" then
luci.http.write(output)
luci.http.write("\n")
else
luci.http.write("No data found")
luci.http.write("\n")
end
end

View file

@ -1,42 +0,0 @@
-- Copyright 2017 Florian Eckert <fe@dev.tdt.de>
-- Licensed to the public under the GNU General Public License v2.
local net = require "luci.model.network".init()
local s, m, o
m = Map("mwan3", translate("MWAN - Globals"))
s = m:section(NamedSection, "globals", "globals", nil)
o = s:option(Value, "mmx_mask",
translate("Firewall mask"),
translate("Enter value in hex, starting with <code>0x</code>"))
o.datatype = "hex(4)"
o.default = "0x3F00"
o = s:option(Flag, "logging",
translate("Logging"),
translate("Enables global firewall logging"))
o = s:option(ListValue, "loglevel",
translate("Loglevel"),
translate("Firewall loglevel"))
o.default = "notice"
o:value("emerg", translate("Emergency"))
o:value("alert", translate("Alert"))
o:value("crit", translate("Critical"))
o:value("error", translate("Error"))
o:value("warning", translate("Warning"))
o:value("notice", translate("Notice"))
o:value("info", translate("Info"))
o:value("debug", translate("Debug"))
o:depends("logging", "1")
o = s:option(DynamicList, "rt_table_lookup",
translate("Routing table lookup"),
translate("Also scan this Routing table for connected networks"))
o.datatype = "integer"
o:value("220", translatef("Routing table %d", 220))
return m

View file

@ -1,241 +0,0 @@
-- Copyright 2014 Aedan Renner <chipdankly@gmail.com
-- Copyright 2018 Florian Eckert <fe@dev.tdt.de>
-- Licensed to the public under the GNU General Public License v2.
local dsp = require "luci.dispatcher"
local uci = require "uci"
local m, mwan_interface, enabled, track_method, reliability, interval
local down, up, metric
function interfaceWarnings(overview, count, iface_max)
local warnings = ""
if count <= iface_max then
warnings = string.format("<strong>%s</strong><br />",
translatef("There are currently %d of %d supported interfaces configured", count, iface_max)
)
else
warnings = string.format("<strong>%s</strong><br />",
translatef("WARNING: %d interfaces are configured exceeding the maximum of %d!", count, iface_max)
)
end
for i, k in pairs(overview) do
if overview[i]["network"] == false then
warnings = warnings .. string.format("<strong>%s</strong><br />",
translatef("WARNING: Interface %s are not found in /etc/config/network", i)
)
end
if overview[i]["default_route"] == false then
warnings = warnings .. string.format("<strong>%s</strong><br />",
translatef("WARNING: Interface %s has no default route in the main routing table", i)
)
end
if overview[i]["reliability"] == false then
warnings = warnings .. string.format("<strong>%s</strong><br />",
translatef("WARNING: Interface %s has a higher reliability " ..
"requirement than tracking hosts (%d)", i, overview[i]["tracking"])
)
end
if overview[i]["duplicate_metric"] == true then
warnings = warnings .. string.format("<strong>%s</strong><br />",
translatef("WARNING: Interface %s has a duplicate metric %s configured", i, overview[i]["metric"])
)
end
end
return warnings
end
function configCheck()
local overview = {}
local count = 0
local duplicate_metric = {}
uci.cursor():foreach("mwan3", "interface",
function (section)
local uci = uci.cursor(nil, "/var/state")
local iface = section[".name"]
overview[iface] = {}
count = count + 1
local network = uci:get("network", iface)
overview[iface]["network"] = false
if network ~= nil then
overview[iface]["network"] = true
local device = uci:get("network", iface, "ifname")
if device ~= nil then
overview[iface]["device"] = device
end
local metric = uci:get("network", iface, "metric")
if metric ~= nil then
overview[iface]["metric"] = metric
overview[iface]["duplicate_metric"] = false
for _, m in ipairs(duplicate_metric) do
if m == metric then
overview[iface]["duplicate_metric"] = true
end
end
table.insert(duplicate_metric, metric)
end
local dump = require("luci.util").ubus("network.interface.%s" % iface, "status", {})
overview[iface]["default_route"] = false
if dump and dump.route then
local _, route
for _, route in ipairs(dump.route) do
if dump.route[_].target == "0.0.0.0" then
overview[iface]["default_route"] = true
end
end
end
end
local trackingNumber = uci:get("mwan3", iface, "track_ip")
overview[iface]["tracking"] = 0
if trackingNumber and #trackingNumber > 0 then
overview[iface]["tracking"] = #trackingNumber
overview[iface]["reliability"] = false
local reliabilityNumber = tonumber(uci:get("mwan3", iface, "reliability") or "1")
if reliabilityNumber and reliabilityNumber <= #trackingNumber then
overview[iface]["reliability"] = true
end
end
end
)
-- calculate iface_max usage from firewall mmx_mask
function bit(p)
return 2 ^ (p - 1)
end
function hasbit(x, p)
return x % (p + p) >= p
end
function setbit(x, p)
return hasbit(x, p) and x or x + p
end
local uci = require("uci").cursor(nil, "/var/state")
local mmx_mask = uci:get("mwan3", "globals", "mmx_mask") or "0x3F00"
local number = tonumber(mmx_mask, 16)
local bits = 0
local iface_max = 0
for i=1,16 do
if hasbit(number, bit(i)) then
bits = bits + 1
iface_max = setbit( iface_max, bit(bits))
end
end
-- subtract blackhole, unreachable and default table from iface_max
iface_max = iface_max - 3
return overview, count, iface_max
end
m = Map("mwan3", translate("MWAN - Interfaces"),
interfaceWarnings(configCheck()))
mwan_interface = m:section(TypedSection, "interface", nil,
translate("mwan3 requires that all interfaces have a unique metric configured in /etc/config/network<br />" ..
"Names must match the interface name found in /etc/config/network<br />" ..
"Names may contain characters A-Z, a-z, 0-9, _ and no spaces<br />" ..
"Interfaces may not share the same name as configured members, policies or rules"))
mwan_interface.addremove = true
mwan_interface.dynamic = false
mwan_interface.sectionhead = translate("Interface")
mwan_interface.sortable = false
mwan_interface.template = "cbi/tblsection"
mwan_interface.extedit = dsp.build_url("admin", "network", "mwan", "interface", "%s")
function mwan_interface.create(self, section)
TypedSection.create(self, section)
m.uci:save("mwan3")
luci.http.redirect(dsp.build_url("admin", "network", "mwan", "interface", section))
end
enabled = mwan_interface:option(DummyValue, "enabled", translate("Enabled"))
enabled.rawhtml = true
function enabled.cfgvalue(self, s)
if self.map:get(s, "enabled") == "1" then
return translate("Yes")
else
return translate("No")
end
end
track_method = mwan_interface:option(DummyValue, "track_method", translate("Tracking method"))
track_method.rawhtml = true
function track_method.cfgvalue(self, s)
local tracked = self.map:get(s, "track_ip")
if tracked then
return self.map:get(s, "track_method") or "ping"
else
return "&#8212;"
end
end
reliability = mwan_interface:option(DummyValue, "reliability", translate("Tracking reliability"))
reliability.rawhtml = true
function reliability.cfgvalue(self, s)
local tracked = self.map:get(s, "track_ip")
if tracked then
return self.map:get(s, "reliability") or "1"
else
return "&#8212;"
end
end
interval = mwan_interface:option(DummyValue, "interval", translate("Ping interval"))
interval.rawhtml = true
function interval.cfgvalue(self, s)
local tracked = self.map:get(s, "track_ip")
if tracked then
local intervalValue = self.map:get(s, "interval")
if intervalValue then
return intervalValue .. "s"
else
return "5s"
end
else
return "&#8212;"
end
end
down = mwan_interface:option(DummyValue, "down", translate("Interface down"))
down.rawhtml = true
function down.cfgvalue(self, s)
local tracked = self.map:get(s, "track_ip")
if tracked then
return self.map:get(s, "down") or "3"
else
return "&#8212;"
end
end
up = mwan_interface:option(DummyValue, "up", translate("Interface up"))
up.rawhtml = true
function up.cfgvalue(self, s)
local tracked = self.map:get(s, "track_ip")
if tracked then
return self.map:get(s, "up") or "3"
else
return "&#8212;"
end
end
metric = mwan_interface:option(DummyValue, "metric", translate("Metric"))
metric.rawhtml = true
function metric.cfgvalue(self, s)
local uci = uci.cursor(nil, "/var/state")
local metric = uci:get("network", s, "metric")
if metric then
return metric
else
return "&#8212;"
end
end
return m

View file

@ -1,262 +0,0 @@
-- Copyright 2014 Aedan Renner <chipdankly@gmail.com>
-- Copyright 2018 Florian Eckert <fe@dev.tdt.de>
-- Licensed to the public under the GNU General Public License v2.
local dsp = require "luci.dispatcher"
local m, mwan_interface, enabled, initial_state, family, track_ip
local track_method, reliability, count, size, max_ttl
local check_quality, failure_latency, failure_loss, recovery_latency
local recovery_loss, timeout, interval, failure
local keep_failure, recovery, down, up, flush, metric
local httping_ssl
arg[1] = arg[1] or ""
m = Map("mwan3", translatef("MWAN Interface Configuration - %s", arg[1]))
m.redirect = dsp.build_url("admin", "network", "mwan", "interface")
mwan_interface = m:section(NamedSection, arg[1], "interface", "")
mwan_interface.addremove = false
mwan_interface.dynamic = false
enabled = mwan_interface:option(Flag, "enabled", translate("Enabled"))
enabled.default = false
initial_state = mwan_interface:option(ListValue, "initial_state", translate("Initial state"),
translate("Expect interface state on up event"))
initial_state.default = "online"
initial_state:value("online", translate("Online"))
initial_state:value("offline", translate("Offline"))
family = mwan_interface:option(ListValue, "family", translate("Internet Protocol"))
family.default = "ipv4"
family:value("ipv4", translate("IPv4"))
family:value("ipv6", translate("IPv6"))
track_ip = mwan_interface:option(DynamicList, "track_ip", translate("Tracking hostname or IP address"),
translate("This hostname or IP address will be pinged to determine if the link is up or down. Leave blank to assume interface is always online"))
track_ip.datatype = "host"
track_method = mwan_interface:option(ListValue, "track_method", translate("Tracking method"))
track_method.default = "ping"
track_method:value("ping")
if os.execute("command -v nping 1>/dev/null") == 0 then
track_method:value("nping-tcp")
track_method:value("nping-udp")
track_method:value("nping-icmp")
track_method:value("nping-arp")
end
if os.execute("command -v arping 1>/dev/null") == 0 then
track_method:value("arping")
end
if os.execute("command -v httping 1>/dev/null") == 0 then
track_method:value("httping")
end
httping_ssl = mwan_interface:option(Flag, "httping_ssl", translate("Enable ssl tracking"),
translate("Enables https tracking on ssl port 443"))
httping_ssl:depends("track_method", "httping")
httping_ssl.rmempty = false
httping_ssl.default = httping_ssl.enabled
reliability = mwan_interface:option(Value, "reliability", translate("Tracking reliability"),
translate("Acceptable values: 1-100. This many Tracking IP addresses must respond for the link to be deemed up"))
reliability.datatype = "range(1, 100)"
reliability.default = "1"
count = mwan_interface:option(ListValue, "count", translate("Ping count"))
count.default = "1"
count:value("1")
count:value("2")
count:value("3")
count:value("4")
count:value("5")
size = mwan_interface:option(Value, "size", translate("Ping size"))
size.default = "56"
size:depends("track_method", "ping")
size:value("8")
size:value("24")
size:value("56")
size:value("120")
size:value("248")
size:value("504")
size:value("1016")
size:value("1472")
size:value("2040")
size.datatype = "range(1, 65507)"
max_ttl = mwan_interface:option(Value, "max_ttl", translate("Max TTL"))
max_ttl.default = "60"
max_ttl:depends("track_method", "ping")
max_ttl:value("10")
max_ttl:value("20")
max_ttl:value("30")
max_ttl:value("40")
max_ttl:value("50")
max_ttl:value("60")
max_ttl:value("70")
max_ttl.datatype = "range(1, 255)"
check_quality = mwan_interface:option(Flag, "check_quality", translate("Check link quality"))
check_quality:depends("track_method", "ping")
check_quality.default = false
failure_latency = mwan_interface:option(Value, "failure_latency", translate("Failure latency [ms]"))
failure_latency:depends("check_quality", 1)
failure_latency.default = "1000"
failure_latency:value("25")
failure_latency:value("50")
failure_latency:value("75")
failure_latency:value("100")
failure_latency:value("150")
failure_latency:value("200")
failure_latency:value("250")
failure_latency:value("300")
failure_loss = mwan_interface:option(Value, "failure_loss", translate("Failure packet loss [%]"))
failure_loss:depends("check_quality", 1)
failure_loss.default = "40"
failure_loss:value("2")
failure_loss:value("5")
failure_loss:value("10")
failure_loss:value("20")
failure_loss:value("25")
recovery_latency = mwan_interface:option(Value, "recovery_latency", translate("Recovery latency [ms]"))
recovery_latency:depends("check_quality", 1)
recovery_latency.default = "500"
recovery_latency:value("25")
recovery_latency:value("50")
recovery_latency:value("75")
recovery_latency:value("100")
recovery_latency:value("150")
recovery_latency:value("200")
recovery_latency:value("250")
recovery_latency:value("300")
recovery_loss = mwan_interface:option(Value, "recovery_loss", translate("Recovery packet loss [%]"))
recovery_loss:depends("check_quality", 1)
recovery_loss.default = "10"
recovery_loss:value("2")
recovery_loss:value("5")
recovery_loss:value("10")
recovery_loss:value("20")
recovery_loss:value("25")
timeout = mwan_interface:option(ListValue, "timeout", translate("Ping timeout"))
timeout.default = "4"
timeout:value("1", translatef("%d second", 1))
timeout:value("2", translatef("%d seconds", 2))
timeout:value("3", translatef("%d seconds", 3))
timeout:value("4", translatef("%d seconds", 4))
timeout:value("5", translatef("%d seconds", 5))
timeout:value("6", translatef("%d seconds", 6))
timeout:value("7", translatef("%d seconds", 7))
timeout:value("8", translatef("%d seconds", 8))
timeout:value("9", translatef("%d seconds", 9))
timeout:value("10", translatef("%d seconds", 10))
interval = mwan_interface:option(ListValue, "interval", translate("Ping interval"))
interval.default = "10"
interval:value("1", translatef("%d second", 1))
interval:value("3", translatef("%d seconds", 3))
interval:value("5", translatef("%d seconds", 5))
interval:value("10", translatef("%d seconds", 10))
interval:value("20", translatef("%d seconds", 20))
interval:value("30", translatef("%d seconds", 30))
interval:value("60", translatef("%d minute", 1))
interval:value("300", translatef("%d minutes", 5))
interval:value("600", translatef("%d minutes", 10))
interval:value("900", translatef("%d minutes", 15))
interval:value("1800", translatef("%d minutes", 30))
interval:value("3600", translatef("%d hour", 1))
failure = mwan_interface:option(Value, "failure_interval", translate("Failure interval"),
translate("Ping interval during failure detection"))
failure.default = "5"
failure:value("1", translatef("%d second", 1))
failure:value("3", translatef("%d seconds", 3))
failure:value("5", translatef("%d seconds", 5))
failure:value("10", translatef("%d seconds", 10))
failure:value("20", translatef("%d seconds", 20))
failure:value("30", translatef("%d seconds", 30))
failure:value("60", translatef("%d minute", 1))
failure:value("300", translatef("%d minutes", 5))
failure:value("600", translatef("%d minutes", 10))
failure:value("900", translatef("%d minutes", 15))
failure:value("1800", translatef("%d minutes", 30))
failure:value("3600", translatef("%d hour", 1))
keep_failure = mwan_interface:option(Flag, "keep_failure_interval", translate("Keep failure interval"),
translate("Keep ping failure interval during failure state"))
keep_failure.default = keep_failure.disabled
recovery = mwan_interface:option(Value, "recovery_interval", translate("Recovery interval"),
translate("Ping interval during failure recovering"))
recovery.default = "5"
recovery:value("1", translatef("%d second", 1))
recovery:value("3", translatef("%d seconds", 3))
recovery:value("5", translatef("%d seconds", 5))
recovery:value("10", translatef("%d seconds", 10))
recovery:value("20", translatef("%d seconds", 20))
recovery:value("30", translatef("%d seconds", 30))
recovery:value("60", translatef("%d minute", 1))
recovery:value("300", translatef("%d minutes", 5))
recovery:value("600", translatef("%d minutes", 10))
recovery:value("900", translatef("%d minutes", 15))
recovery:value("1800", translatef("%d minutes", 30))
recovery:value("3600", translatef("%d hour", 1))
down = mwan_interface:option(ListValue, "down", translate("Interface down"),
translate("Interface will be deemed down after this many failed ping tests"))
down.default = "5"
down:value("1")
down:value("2")
down:value("3")
down:value("4")
down:value("5")
down:value("6")
down:value("7")
down:value("8")
down:value("9")
down:value("10")
up = mwan_interface:option(ListValue, "up", translate("Interface up"),
translate("Downed interface will be deemed up after this many successful ping tests"))
up.default = "5"
up:value("1")
up:value("2")
up:value("3")
up:value("4")
up:value("5")
up:value("6")
up:value("7")
up:value("8")
up:value("9")
up:value("10")
flush = mwan_interface:option(StaticList, "flush_conntrack", translate("Flush conntrack table"),
translate("Flush global firewall conntrack table on interface events"))
flush:value("ifup", translate("ifup (netifd)"))
flush:value("ifdown", translate("ifdown (netifd)"))
flush:value("connected", translate("connected (mwan3)"))
flush:value("disconnected", translate("disconnected (mwan3)"))
metric = mwan_interface:option(DummyValue, "metric", translate("Metric"),
translate("This displays the metric assigned to this interface in /etc/config/network"))
metric.rawhtml = true
function metric.cfgvalue(self, s)
local uci = require "luci.model.uci".cursor(nil, "/var/state")
local metric = uci:get("network", arg[1], "metric")
if metric then
return metric
else
return "&#8212;"
end
end
return m

View file

@ -1,45 +0,0 @@
-- Copyright 2014 Aedan Renner <chipdankly@gmail.com>
-- Copyright 2018 Florian Eckert <fe@dev.tdt.de>
-- Licensed to the public under the GNU General Public License v2.
local dsp = require "luci.dispatcher"
local m, s, o
m = Map("mwan3", translate("MWAN - Members"))
s = m:section(TypedSection, "member", nil,
translate("Members are profiles attaching a metric and weight to an MWAN interface<br />" ..
"Names may contain characters A-Z, a-z, 0-9, _ and no spaces<br />" ..
"Members may not share the same name as configured interfaces, policies or rules"))
s.addremove = true
s.dynamic = false
s.sectionhead = translate("Member")
s.sortable = true
s.template = "cbi/tblsection"
s.extedit = dsp.build_url("admin", "network", "mwan", "member", "%s")
function s.create(self, section)
TypedSection.create(self, section)
m.uci:save("mwan3")
luci.http.redirect(dsp.build_url("admin", "network", "mwan", "member", section))
end
o = s:option(DummyValue, "interface", translate("Interface"))
o.rawhtml = true
function o.cfgvalue(self, s)
return self.map:get(s, "interface") or "&#8212;"
end
o = s:option(DummyValue, "metric", translate("Metric"))
o.rawhtml = true
function o.cfgvalue(self, s)
return self.map:get(s, "metric") or "1"
end
o = s:option(DummyValue, "weight", translate("Weight"))
o.rawhtml = true
function o.cfgvalue(self, s)
return self.map:get(s, "weight") or "1"
end
return m

View file

@ -1,33 +0,0 @@
-- Copyright 2014 Aedan Renner <chipdankly@gmail.com>
-- Copyright 2018 Florian Eckert <fe@dev.tdt.de>
-- Licensed to the public under the GNU General Public License v2.
local dsp = require "luci.dispatcher"
local m, mwan_member, interface, metric, weight
arg[1] = arg[1] or ""
m = Map("mwan3", translatef("MWAN Member Configuration - %s", arg[1]))
m.redirect = dsp.build_url("admin", "network", "mwan", "member")
mwan_member = m:section(NamedSection, arg[1], "member", "")
mwan_member.addremove = false
mwan_member.dynamic = false
interface = mwan_member:option(Value, "interface", translate("Interface"))
m.uci:foreach("mwan3", "interface",
function(s)
interface:value(s['.name'], s['.name'])
end
)
metric = mwan_member:option(Value, "metric", translate("Metric"),
translate("Acceptable values: 1-256. Defaults to 1 if not set"))
metric.datatype = "range(1, 256)"
weight = mwan_member:option(Value, "weight", translate("Weight"),
translate("Acceptable values: 1-1000. Defaults to 1 if not set"))
weight.datatype = "range(1, 1000)"
return m

View file

@ -1,46 +0,0 @@
-- Copyright 2014 Aedan Renner <chipdankly@gmail.com>
-- Copyright 2018 Florian Eckert <fe@dev.tdt.de>
-- Licensed to the public under the GNU General Public License v2.
local fs = require "nixio.fs"
local ut = require "luci.util"
local script = "/etc/mwan3.user"
local m, f, t
m = SimpleForm("luci", translate("MWAN - Notification"))
f = m:section(SimpleSection, nil,
translate("This section allows you to modify the content of \"/etc/mwan3.user\".<br />" ..
"The file is also preserved during sysupgrade.<br />" ..
"<br />" ..
"Notes:<br />" ..
"This file is interpreted as a shell script.<br />" ..
"The first line of the script must be &#34;#!/bin/sh&#34; without quotes.<br />" ..
"Lines beginning with # are comments and are not executed.<br />" ..
"Put your custom mwan3 action here, they will<br />" ..
"be executed with each netifd hotplug interface event<br />" ..
"on interfaces for which mwan3 is enabled.<br />" ..
"<br />" ..
"There are three main environment variables that are passed to this script.<br />" ..
"<br />" ..
"$ACTION <br />" ..
"* \"ifup\" Is called by netifd and mwan3track <br />" ..
"* \"ifdown\" Is called by netifd and mwan3track <br />" ..
"* \"connected\" Is only called by mwan3track if tracking was successful <br />" ..
"* \"disconnected\" Is only called by mwan3track if tracking has failed <br />" ..
"$INTERFACE Name of the interface which went up or down (e.g. \"wan\" or \"wwan\")<br />" ..
"$DEVICE Physical device name which interface went up or down (e.g. \"eth0\" or \"wwan0\")<br />" ..
"<br />"))
t = f:option(TextValue, "lines")
t.rmempty = true
t.rows = 20
function t.cfgvalue()
return fs.readfile(script)
end
function t.write(self, section, data)
return fs.writefile(script, ut.trim(data:gsub("\r\n", "\n")) .. "\n")
end
return m

View file

@ -1,92 +0,0 @@
-- Copyright 2014 Aedan Renner <chipdankly@gmail.com>
-- Copyright 2018 Florian Eckert <fe@dev.tdt.de>
-- Licensed to the public under the GNU General Public License v2.
local dsp = require "luci.dispatcher"
local uci = require "uci"
local m, s, o
function policyCheck()
local policy_error = {}
uci.cursor():foreach("mwan3", "policy",
function (section)
policy_error[section[".name"]] = false
if string.len(section[".name"]) > 15 then
policy_error[section[".name"]] = true
end
end
)
return policy_error
end
function policyError(policy_error)
local warnings = ""
for i, k in pairs(policy_error) do
if policy_error[i] == true then
warnings = warnings .. string.format("<strong>%s</strong><br />",
translatef("WARNING: Policy %s has exceeding the maximum name of 15 characters", i)
)
end
end
return warnings
end
m = Map("mwan3", translate("MWAN - Policies"),
policyError(policyCheck()))
s = m:section(TypedSection, "policy", nil,
translate("Policies are profiles grouping one or more members controlling how MWAN distributes traffic<br />" ..
"Member interfaces with lower metrics are used first<br />" ..
"Member interfaces with the same metric will be load-balanced<br />" ..
"Load-balanced member interfaces distribute more traffic out those with higher weights<br />" ..
"Names may contain characters A-Z, a-z, 0-9, _ and no spaces<br />" ..
"Names must be 15 characters or less<br />" ..
"Policies may not share the same name as configured interfaces, members or rules"))
s.addremove = true
s.dynamic = false
s.sectionhead = translate("Policy")
s.sortable = true
s.template = "cbi/tblsection"
s.extedit = dsp.build_url("admin", "network", "mwan", "policy", "%s")
function s.create(self, section)
if #section > 15 then
self.invalid_cts = true
else
TypedSection.create(self, section)
m.uci:save("mwan3")
luci.http.redirect(dsp.build_url("admin", "network", "mwan", "policy", section))
end
end
o = s:option(DummyValue, "use_member", translate("Members assigned"))
o.rawhtml = true
function o.cfgvalue(self, s)
local memberConfig, memberList = self.map:get(s, "use_member"), ""
if memberConfig then
for k,v in pairs(memberConfig) do
memberList = memberList .. v .. "<br />"
end
return memberList
else
return "&#8212;"
end
end
o = s:option(DummyValue, "last_resort", translate("Last resort"))
o.rawhtml = true
function o.cfgvalue(self, s)
local action = self.map:get(s, "last_resort")
if action == "blackhole" then
return translate("blackhole (drop)")
elseif action == "default" then
return translate("default (use main routing table)")
else
return translate("unreachable (reject)")
end
end
return m

View file

@ -1,32 +0,0 @@
-- Copyright 2014 Aedan Renner <chipdankly@gmail.com>
-- Copyright 2018 Florian Eckert <fe@dev.tdt.de>
-- Licensed to the public under the GNU General Public License v2.
local dsp = require "luci.dispatcher"
local m, mwan_policy, member, last_resort
arg[1] = arg[1] or ""
m = Map("mwan3", translatef("MWAN Policy Configuration - %s", arg[1]))
m.redirect = dsp.build_url("admin", "network", "mwan", "policy")
mwan_policy = m:section(NamedSection, arg[1], "policy", "")
mwan_policy.addremove = false
mwan_policy.dynamic = false
member = mwan_policy:option(DynamicList, "use_member", translate("Member used"))
m.uci:foreach("mwan3", "member",
function(s)
member:value(s['.name'], s['.name'])
end
)
last_resort = mwan_policy:option(ListValue, "last_resort", translate("Last resort"),
translate("When all policy members are offline use this behavior for matched traffic"))
last_resort.default = "unreachable"
last_resort:value("unreachable", translate("unreachable (reject)"))
last_resort:value("blackhole", translate("blackhole (drop)"))
last_resort:value("default", translate("default (use main routing table)"))
return m

View file

@ -1,109 +0,0 @@
-- Copyright 2014 Aedan Renner <chipdankly@gmail.com>
-- Copyright 2018 Florian Eckert <fe@dev.tdt.de>
-- Licensed to the public under the GNU General Public License v2.
local dsp = require "luci.dispatcher"
local uci = require "uci"
local m, mwan_rule, src_ip, src_port, dest_ip, dest_port, proto, use_policy
function ruleCheck()
local rule_error = {}
uci.cursor():foreach("mwan3", "rule",
function (section)
rule_error[section[".name"]] = false
local uci = uci.cursor(nil, "/var/state")
local sourcePort = uci:get("mwan3", section[".name"], "src_port")
local destPort = uci:get("mwan3", section[".name"], "dest_port")
if sourcePort ~= nil or destPort ~= nil then
local protocol = uci:get("mwan3", section[".name"], "proto")
if protocol == nil or protocol == "all" then
rule_error[section[".name"]] = true
end
end
end
)
return rule_error
end
function ruleWarn(rule_error)
local warnings = ""
for i, k in pairs(rule_error) do
if rule_error[i] == true then
warnings = warnings .. string.format("<strong>%s</strong><br />",
translatef("WARNING: Rule %s have a port configured with no or improper protocol specified!", i)
)
end
end
return warnings
end
m = Map("mwan3", translate("MWAN - Rules"),
ruleWarn(ruleCheck())
)
mwan_rule = m:section(TypedSection, "rule", nil,
translate("Rules specify which traffic will use a particular MWAN policy<br />" ..
"Rules are based on IP address, port or protocol<br />" ..
"Rules are matched from top to bottom<br />" ..
"Rules below a matching rule are ignored<br />" ..
"Traffic not matching any rule is routed using the main routing table<br />" ..
"Traffic destined for known (other than default) networks is handled by the main routing table<br />" ..
"Traffic matching a rule, but all WAN interfaces for that policy are down will be blackholed<br />" ..
"Names may contain characters A-Z, a-z, 0-9, _ and no spaces<br />" ..
"Rules may not share the same name as configured interfaces, members or policies"))
mwan_rule.addremove = true
mwan_rule.anonymous = false
mwan_rule.dynamic = false
mwan_rule.sectionhead = translate("Rule")
mwan_rule.sortable = true
mwan_rule.template = "cbi/tblsection"
mwan_rule.extedit = dsp.build_url("admin", "network", "mwan", "rule", "%s")
function mwan_rule.create(self, section)
if #section > 15 then
self.invalid_cts = true
else
TypedSection.create(self, section)
m.uci:save("mwan3")
luci.http.redirect(dsp.build_url("admin", "network", "mwan", "rule", section))
end
end
src_ip = mwan_rule:option(DummyValue, "src_ip", translate("Source address"))
src_ip.rawhtml = true
function src_ip.cfgvalue(self, s)
return self.map:get(s, "src_ip") or "&#8212;"
end
src_port = mwan_rule:option(DummyValue, "src_port", translate("Source port"))
src_port.rawhtml = true
function src_port.cfgvalue(self, s)
return self.map:get(s, "src_port") or "&#8212;"
end
dest_ip = mwan_rule:option(DummyValue, "dest_ip", translate("Destination address"))
dest_ip.rawhtml = true
function dest_ip.cfgvalue(self, s)
return self.map:get(s, "dest_ip") or "&#8212;"
end
dest_port = mwan_rule:option(DummyValue, "dest_port", translate("Destination port"))
dest_port.rawhtml = true
function dest_port.cfgvalue(self, s)
return self.map:get(s, "dest_port") or "&#8212;"
end
proto = mwan_rule:option(DummyValue, "proto", translate("Protocol"))
proto.rawhtml = true
function proto.cfgvalue(self, s)
return self.map:get(s, "proto") or "all"
end
use_policy = mwan_rule:option(DummyValue, "use_policy", translate("Policy assigned"))
use_policy.rawhtml = true
function use_policy.cfgvalue(self, s)
return self.map:get(s, "use_policy") or "&#8212;"
end
return m

View file

@ -1,85 +0,0 @@
-- Copyright 2014 Aedan Renner <chipdankly@gmail.com>
-- Copyright 2018 Florian Eckert <fe@dev.tdt.de>
-- Licensed to the public under the GNU General Public License v2.
local dsp = require "luci.dispatcher"
local util = require("luci.util")
local m, s, o
arg[1] = arg[1] or ""
local ipsets = util.split(util.trim(util.exec("ipset -n -L 2>/dev/null | grep -v mwan3_ | sort")), "\n", nil, true) or {}
m = Map("mwan3", translatef("MWAN Rule Configuration - %s", arg[1]))
m.redirect = dsp.build_url("admin", "network", "mwan", "rule")
s = m:section(NamedSection, arg[1], "rule", "")
s.addremove = false
s.dynamic = false
o = s:option(ListValue, "family", translate("Internet Protocol"))
o.default = ""
o:value("", translate("IPv4 and IPv6"))
o:value("ipv4", translate("IPv4 only"))
o:value("ipv6", translate("IPv6 only"))
o = s:option(Value, "src_ip", translate("Source address"),
translate("Supports CIDR notation (eg \"192.168.100.0/24\") without quotes"))
o.datatype = ipaddr
o = s:option(Value, "src_port", translate("Source port"),
translate("May be entered as a single or multiple port(s) (eg \"22\" or \"80,443\") or as a portrange (eg \"1024:2048\") without quotes"))
o:depends("proto", "tcp")
o:depends("proto", "udp")
o = s:option(Value, "dest_ip", translate("Destination address"),
translate("Supports CIDR notation (eg \"192.168.100.0/24\") without quotes"))
o.datatype = ipaddr
o = s:option(Value, "dest_port", translate("Destination port"),
translate("May be entered as a single or multiple port(s) (eg \"22\" or \"80,443\") or as a portrange (eg \"1024:2048\") without quotes"))
o:depends("proto", "tcp")
o:depends("proto", "udp")
o = s:option(Value, "proto", translate("Protocol"),
translate("View the content of /etc/protocols for protocol description"))
o.default = "all"
o.rmempty = false
o:value("all")
o:value("tcp")
o:value("udp")
o:value("icmp")
o:value("esp")
o = s:option(ListValue, "sticky", translate("Sticky"),
translate("Traffic from the same source IP address that previously matched this rule within the sticky timeout period will use the same WAN interface"))
o.default = "0"
o:value("1", translate("Yes"))
o:value("0", translate("No"))
o = s:option(Value, "timeout", translate("Sticky timeout"),
translate("Seconds. Acceptable values: 1-1000000. Defaults to 600 if not set"))
o.datatype = "range(1, 1000000)"
o = s:option(Value, "ipset", translate("IPset"),
translate("Name of IPset rule. Requires IPset rule in /etc/dnsmasq.conf (eg \"ipset=/youtube.com/youtube\")"))
o:value("", translate("-- Please choose --"))
for _, z in ipairs(ipsets) do
o:value(z)
end
o = s:option(Flag, "logging", translate("Logging"),
translate("Enables firewall rule logging (global mwan3 logging must also be enabled)"))
o = s:option(Value, "use_policy", translate("Policy assigned"))
m.uci:foreach("mwan3", "policy",
function(s)
o:value(s['.name'], s['.name'])
end
)
o:value("unreachable", translate("unreachable (reject)"))
o:value("blackhole", translate("blackhole (drop)"))
o:value("default", translate("default (use main routing table)"))
return m

View file

@ -1,3 +0,0 @@
<%if require("luci.sys").init.enabled("mwan3") then%>
<%+mwan/overview_status_interface%>
<%end%>

View file

@ -1,114 +0,0 @@
<%#
Copyright 2014 Aedan Renner <chipdankly@gmail.com>
Copyright 2018 Florian Eckert <fe@dev.tdt.de>
Licensed to the public under the GNU General Public License v2.
-%>
<script type="text/javascript">//<![CDATA[
function secondsToString(time) {
var seconds = parseInt(time, 10);
var hrs = Math.floor(seconds / 3600);
seconds -= hrs*3600;
var mnts = Math.floor(seconds / 60);
seconds -= mnts*60;
return String.format("%sh:%sm:%ss", hrs, mnts, seconds);
}
XHR.poll(-1, '<%=luci.dispatcher.build_url("admin", "status", "mwan", "interface_status")%>', null,
function(x, status)
{
var statusDiv = document.getElementById('mwan_status_text');
if (status.interfaces)
{
var statusview = '';
for ( var iface in status.interfaces)
{
var state = '';
var css = '';
var time = '';
switch (status.interfaces[iface].status)
{
case 'online':
state = '<%:Online%>';
time = String.format(
'<div><strong><%:Uptime%>:&nbsp;</strong>%s</div>',
secondsToString(status.interfaces[iface].online)
);
css = 'success';
break;
case 'offline':
state = '<%:Offline%>';
time = String.format(
'<div><strong><%:Downtime%>:&nbsp;</strong>%s</div>',
secondsToString(status.interfaces[iface].offline)
);
css = 'danger';
break;
case 'notracking':
state = '<%:No Tracking%>';
if ((status.interfaces[iface].uptime) > 0) {
time = String.format(
'<div><strong><%:Uptime%>:&nbsp;</strong>%s</div>',
secondsToString(status.interfaces[iface].uptime)
);
css = 'success';
}
else {
time = '<div>&nbsp;</div>'
css = 'warning';
}
break;
default:
state = '<%:Disabled%>';
time = '<div>&nbsp;</div>'
css = 'warning';
break;
}
statusview += String.format(
'<div class="alert-message %s">',
css
);
statusview += String.format(
'<div><strong><%:Interface%>:&nbsp;</strong>%s</div>',
iface
);
statusview += String.format(
'<div><strong><%:Status%>:&nbsp;</strong>%s</div>',
state
);
if (time)
{
statusview += time;
}
statusview += '</div>'
}
statusDiv.innerHTML = statusview;
}
else
{
statusDiv.innerHTML = '<strong><%:No MWAN interfaces found%></strong>';
}
}
);
//]]></script>
<style type="text/css">
#mwan_status_text > div {
display: inline-block;
margin: 1rem;
padding: 1rem;
width: 15rem;
float: left;
line-height: 125%;
}
</style>
<fieldset id="interface_field" class="cbi-section">
<legend><%:MWAN Interfaces%></legend>
<div id="mwan_status_text">
<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align:middle" />
<%:Collecting data...%>
</div>
</fieldset>

View file

@ -1,39 +0,0 @@
<%#
Copyright 2014 Aedan Renner <chipdankly@gmail.com>
Copyright 2018 Florian Eckert <fe@dev.tdt.de>
Licensed to the public under the GNU General Public License v2.
-%>
<%+header%>
<ul class="cbi-tabmenu">
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/overview")%>"><%:Interface%></a></li>
<li class="cbi-tab"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/detail")%>"><%:Detail%></a></li>
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/diagnostics")%>"><%:Diagnostics%></a></li>
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/troubleshooting")%>"><%:Troubleshooting%></a></li>
</ul>
<script type="text/javascript">//<![CDATA[
XHR.poll(-1, '<%=luci.dispatcher.build_url("admin", "status", "mwan", "detailed_status")%>', null,
function(x)
{
var output = document.getElementById('diag-rc-output');
output.innerHTML = String.format('<pre>%h</pre>', x.responseText);
}
);
//]]></script>
<div class="cbi-map">
<h2 name="content"><%:MWAN Status - Detail%></h2>
<%if not require("luci.sys").init.enabled("mwan3") then%>
<div><strong><%:INFO: MWAN not running%></strong></div>
<%end%>
<fieldset class="cbi-section">
<span id="diag-rc-output">
<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align: middle;" />
<%:Collecting data...%>
</span>
</fieldset>
</div>
<%+footer%>

View file

@ -1,97 +0,0 @@
<%#
Copyright 2014 Aedan Renner <chipdankly@gmail.com>
Copyright 2018 Florian Eckert <fe@dev.tdt.de>
Licensed to the public under the GNU General Public License v2.
-%>
<%+header%>
<ul class="cbi-tabmenu">
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/overview")%>"><%:Interface%></a></li>
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/detail")%>"><%:Detail%></a></li>
<li class="cbi-tab"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/diagnostics")%>"><%:Diagnostics%></a></li>
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/troubleshooting")%>"><%:Troubleshooting%></a></li>
</ul>
<%
local uci = require "luci.model.uci"
local iface = {}
uci.cursor():foreach("mwan3", "interface",
function (section)
table.insert(iface, section[".name"])
end
)
%>
<script type="text/javascript">//<![CDATA[
var stxhr = new XHR();
function update_status(iface, task)
{
var output = document.getElementById('diag-rc-output');
output.innerHTML =
'<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align: middle;" />' +
"<%:Waiting for command to complete...%>"
;
output.parentNode.style.display = 'block';
output.style.display = 'inline';
stxhr.post('<%=url('admin/status/mwan')%>/diagnostics_display' + '/' + iface + '/' + task, { token: '<%=token%>' },
function(x)
{
output.innerHTML = String.format('<pre>%h</pre>', x.responseText);
}
);
}
//]]></script>
<form method="post" action="<%=url('admin/network/diagnostics')%>">
<div class="cbi-map">
<h2 name="content"><%:MWAN Status - Diagnostics%></h2>
<%if not require("luci.sys").init.enabled("mwan3") then%>
<div><strong><%:INFO: MWAN not running%></strong></div>
<%end%>
<div class="cbi-section">
<div class="cbi-section-node">
<div class="cbi-value">
<label class="cbi-value-title"><%:Interface%></label>
<div class="cbi-value-field">
<select class="cbi-input-select" name="iface">
<% for _, z in ipairs(iface) do -%><option value="<%=z%>"><%=z%></option><%- end %>
</select>
</div>
</div>
</div>
<div class="cbi-section-node">
<div class="cbi-value">
<label class="cbi-value-title"><%:Task%></label>
<div class="cbi-value-field">
<select class="cbi-input-select" name="task">
<option value="ping_gateway"><%:Ping default gateway%></option>
<option value="ping_trackips"><%:Ping tracking IP%></option>
<option value="check_rules"><%:Check IP rules%></option>
<option value="check_routes"><%:Check routing table%></option>
<option value="hotplug_ifup"><%:Hotplug ifup%></option>
<option value="hotplug_ifdown"><%:Hotplug ifdown%></option>
</select>
</div>
</div>
</div>
</div>
<div class="cbi-section-create">
<input type="button" value="<%:Execute%>" class="btn cbi-button cbi-button-apply" onclick="update_status(this.form.iface.value, this.form.task.value)"/>
</div>
<div class="cbi-section" style="display:none">
<span id="diag-rc-output"></span>
</div>
</div>
</form>
<%+footer%>

View file

@ -1,20 +0,0 @@
<%#
Copyright 2014 Aedan Renner <chipdankly@gmail.com>
Copyright 2018 Florian Eckert <fe@dev.tdt.de>
Licensed to the public under the GNU General Public License v2.
-%>
<%+header%>
<ul class="cbi-tabmenu">
<li class="cbi-tab"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/overview")%>"><%:Interface%></a></li>
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/detail")%>"><%:Detail%></a></li>
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/diagnostics")%>"><%:Diagnostics%></a></li>
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/troubleshooting")%>"><%:Troubleshooting%></a></li>
</ul>
<div class="cbi-map">
<%+mwan/overview_status_interface%>
</div>
<%+footer%>

View file

@ -1,39 +0,0 @@
<%#
Copyright 2014 Aedan Renner <chipdankly@gmail.com>
Copyright 2018 Florian Eckert <fe@dev.tdt.de>
Licensed to the public under the GNU General Public License v2.
-%>
<%+header%>
<ul class="cbi-tabmenu">
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/overview")%>"><%:Interface%></a></li>
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/detail")%>"><%:Detail%></a></li>
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/diagnostics")%>"><%:Diagnostics%></a></li>
<li class="cbi-tab"><a href="<%=luci.dispatcher.build_url("admin/status/mwan/troubleshooting")%>"><%:Troubleshooting%></a></li>
</ul>
<script type="text/javascript">//<![CDATA[
XHR.poll(15, '<%=luci.dispatcher.build_url("admin", "status", "mwan", "troubleshooting_display")%>', null,
function(x)
{
var output = document.getElementById('diag-rc-output');
output.innerHTML = String.format('<pre>%h</pre>', x.responseText);
}
);
//]]></script>
<div class="cbi-map">
<h2 name="content"><%:MWAN Status - Troubleshooting%></h2>
<%if not require("luci.sys").init.enabled("mwan3") then%>
<div><strong><%:INFO: MWAN not running%></strong></div>
<%end%>
<fieldset class="cbi-section">
<span id="diag-rc-output">
<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align: middle;" />
<%:Collecting data...%>
</span>
</fieldset>
</div>
<%+footer%>

View file

@ -0,0 +1,199 @@
#!/bin/sh
#
# Copyright (C) 2021 TDT AG <development@tdt.de>
#
# This is free software, licensed under the GNU General Public License v2.
# See https://www.gnu.org/licenses/gpl-2.0.txt for more information.
#
. /lib/functions.sh
. /lib/functions/network.sh
. /usr/share/libubox/jshn.sh
IIF=1000
FWMARK=2000
ID=0
usage() {
local status="$1"
local msg="$2"
if [ -n "$msg" ]; then
echo "$msg"
echo ""
fi
echo "Usage: $(basename "$0") <command>"
echo "command:"
echo " diag: diagnostic commands"
echo " ipset: ipset commands"
echo ""
echo "diag <command> <iface>"
echo "command:"
echo " gateway <iface>: ping interface gateway"
echo " tracking <iface>: ping interface tracking targets"
echo " rules <iface>: check interface routing rules"
echo " routes <iface>: check interface routing tables"
echo ""
echo "ipset <command>"
echo "command:"
echo " dump: show all configured ipset names"
exit "$status"
}
diag_gateway() {
local iface="$1"
local gw
network_get_gateway gw "${iface}"
[ -z "$gw" ] && network_get_gateway gw "${iface}_4"
[ -z "$gw" ] && {
echo "No gateway for interface ${iface} found."
exit 2
}
mwan3 use "$iface" "ping" "-c" "5" "-W" "1" "$gw"
}
diag_tracking() {
local iface="$1"
checkips() {
local ip="$1"
local iface="$2"
mwan3 use "$iface" "ping" "-c" "5" "-W" "1" "$ip"
}
config_load mwan3
config_list_foreach "$iface" "track_ip" checkips "$iface"
}
iface_number() {
local cfg="$1"
local iface="$2"
let number++
[ "$cfg" = "$iface" ] && {
ID="$number"
}
}
diag_rules() {
local iface="$1"
local number=0
local iif=0
local fwmark=0
local iif_rule iif_result
local fwmark_rule fwmark_result
config_load mwan3
config_foreach iface_number 'interface' "$iface"
[ "$ID" = "0" ] && {
echo "Unable to get mwan3 interface number for \"$iface\"."
exit 2
}
let "iif=$IIF+$ID"
let "fwmark=$FWMARK+$ID"
iif_rule="$(ip rule | grep ${iif})"
iif_result="$?"
fwmark_rule="$(ip rule | grep ${fwmark})"
fwmark_result="$?"
if [ "$fwmark_result" = 0 ] && [ "$iif_result" = 0 ]; then
echo "All required IP rules for interface \"$iface\" found"
echo "$fwmark_rule"
echo "$iif_rule"
elif [ "$fwmark_result" = 1 ] && [ "$iif_result" = 0 ]; then
echo "Only iif IP rule for interface \"$iface\" found"
echo "$iif_rule"
elif [ "$fwmark_result" = 0 ] && [ "$iif_result" = 1 ]; then
echo "Only fwmark IP rule for interface \"$iface\" found"
echo "$fwmark_rule"
else
echo "Missing fwmark and iif IP rule for interface \"$iface\""
fi
}
diag_routes() {
local iface="$1"
local table table_result
config_load mwan3
config_foreach iface_number 'interface' "$iface"
[ "$ID" = "0" ] && {
echo "Unable to get mwan3 interface number for \"$iface\"."
exit 2
}
table="$(ip route list table $ID)"
table_result="$?"
if [ "$table_result" = 0 ]; then
echo "Routing table \"$ID\" for interface \"$iface\" found"
echo "$table"
else
echo "Routing table \"$ID\" for interface \"$iface\" not found"
fi
}
diag_cmd() {
case "$1" in
gateway)
diag_gateway "$2"
;;
tracking)
diag_tracking "$2"
;;
rules)
diag_rules "$2"
;;
routes)
diag_routes "$2"
;;
*)
usage "1" "Command not supported"
;;
esac
}
ipset_dump() {
ipset -n -L 2>/dev/null | grep -v mwan3_ | sort -u
}
ipset_cmd() {
case "$1" in
dump)
ipset_dump
;;
*)
usage "1" "Command not supported"
;;
esac
}
main () {
case "$1" in
diag)
diag_cmd "$2" "$3"
;;
ipset)
ipset_cmd "$2"
;;
*)
usage "1" "Command not supported"
;;
esac
}
main "$@"

View file

@ -0,0 +1,103 @@
{
"admin/status/mwan3": {
"title": "MultiWAN Manager",
"order": "600",
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-mwan3" ]
}
},
"admin/status/mwan3/overview": {
"title": "Overview",
"order": 10,
"action": {
"type": "view",
"path": "mwan3/status/overview"
}
},
"admin/status/mwan3/detail": {
"title": "Status",
"order": 20,
"action": {
"type": "view",
"path": "mwan3/status/detail"
}
},
"admin/status/mwan3/diagnostics": {
"title": "Diagnostics",
"order": 30,
"action": {
"type": "view",
"path": "mwan3/status/diagnostics"
}
},
"admin/status/mwan3/troubleshooting": {
"title": "Troubleshooting",
"order": 40,
"action": {
"type": "view",
"path": "mwan3/status/troubleshooting"
}
},
"admin/network/mwan3": {
"title": "MultiWAN Manager",
"order": "600",
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-mwan3" ]
}
},
"admin/network/mwan3/globals": {
"title": "Globals",
"order": 10,
"action": {
"type": "view",
"path": "mwan3/network/globals"
}
},
"admin/network/mwan3/interface": {
"title": "Interface",
"order": 20,
"action": {
"type": "view",
"path": "mwan3/network/interface"
}
},
"admin/network/mwan3/member": {
"title": "Member",
"order": 30,
"action": {
"type": "view",
"path": "mwan3/network/member"
}
},
"admin/network/mwan3/policy": {
"title": "Policy",
"order": 40,
"action": {
"type": "view",
"path": "mwan3/network/policy"
}
},
"admin/network/mwan3/rule": {
"title": "Rule",
"order": 50,
"action": {
"type": "view",
"path": "mwan3/network/rule"
}
},
"admin/network/mwan3/notify": {
"title": "Notify",
"order": 60,
"action": {
"type": "view",
"path": "mwan3/network/notify"
}
}
}

View file

@ -2,9 +2,30 @@
"luci-app-mwan3": {
"description": "Grant UCI access for luci-app-mwan3",
"read": {
"uci": [ "mwan3" ]
"file": {
"/etc/mwan3.user": [ "read" ],
"/usr/bin/httping": [ "list" ],
"/usr/bin/nping": [ "list" ],
"/usr/bin/arping": [ "list" ],
"/usr/sbin/mwan3 status": [ "exec" ],
"/usr/sbin/mwan3 ifup *": [ "exec" ],
"/usr/sbin/mwan3 ifdown *": [ "exec" ],
"/usr/sbin/mwan3 internal ipv4": [ "exec" ],
"/usr/sbin/mwan3 internal ipv6": [ "exec" ],
"/usr/libexec/luci-mwan3 diag * *": [ "exec" ],
"/usr/libexec/luci-mwan3 ipset *": [ "exec" ]
},
"ubus": {
"mwan3": [ "status" ]
},
"uci": [ "mwan3", "network" ]
},
"write": {
"file": {
"/etc/mwan3.user": ["write"],
"/usr/sbin/mwan3 ifup *": [ "exec" ],
"/usr/sbin/mwan3 ifdown *": [ "exec" ]
},
"uci": [ "mwan3" ]
}
}