summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-mod-network
diff options
context:
space:
mode:
Diffstat (limited to 'modules/luci-mod-network')
-rw-r--r--modules/luci-mod-network/Makefile2
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/tools/network.js979
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js843
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/diagnostics.js68
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/hosts.js43
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js1108
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js214
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js26
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js395
-rw-r--r--modules/luci-mod-network/root/usr/share/luci/menu.d/luci-mod-network.json30
-rw-r--r--modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json11
11 files changed, 3056 insertions, 663 deletions
diff --git a/modules/luci-mod-network/Makefile b/modules/luci-mod-network/Makefile
index bf5627d64d..b382d800d9 100644
--- a/modules/luci-mod-network/Makefile
+++ b/modules/luci-mod-network/Makefile
@@ -7,7 +7,7 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI Network Administration
-LUCI_DEPENDS:=+luci-base +libiwinfo-lua +rpcd-mod-iwinfo
+LUCI_DEPENDS:=+luci-base +rpcd-mod-iwinfo
PKG_LICENSE:=Apache-2.0
diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/tools/network.js b/modules/luci-mod-network/htdocs/luci-static/resources/tools/network.js
new file mode 100644
index 0000000000..092bbbc14a
--- /dev/null
+++ b/modules/luci-mod-network/htdocs/luci-static/resources/tools/network.js
@@ -0,0 +1,979 @@
+'use strict';
+'require fs';
+'require ui';
+'require dom';
+'require uci';
+'require form';
+'require network';
+'require baseclass';
+'require validation';
+'require tools.widgets as widgets';
+
+function validateAddr(section_id, value) {
+ if (value == '')
+ return true;
+
+ var ipv6 = /6$/.test(this.section.formvalue(section_id, 'mode')),
+ addr = ipv6 ? validation.parseIPv6(value) : validation.parseIPv4(value);
+
+ return addr ? true : (ipv6 ? _('Expecting a valid IPv6 address') : _('Expecting a valid IPv4 address'));
+}
+
+function validateQoSMap(section_id, value) {
+ if (value == '')
+ return true;
+
+ var m = value.match(/^(\d+):(\d+)$/);
+
+ if (!m || +m[1] > 0xFFFFFFFF || +m[2] > 0xFFFFFFFF)
+ return _('Expecting two priority values separated by a colon');
+
+ return true;
+}
+
+function deviceSectionExists(section_id, devname) {
+ var exists = false;
+
+ uci.sections('network', 'device', function(ss) {
+ exists = exists || (
+ ss['.name'] != section_id &&
+ ss.name == devname
+ );
+ });
+
+ return exists;
+}
+
+function isBridgePort(dev) {
+ if (!dev)
+ return false;
+
+ if (dev.isBridgePort())
+ return true;
+
+ var isPort = false;
+
+ uci.sections('network', null, function(s) {
+ if (s['.type'] != 'interface' && s['.type'] != 'device')
+ return;
+
+ if (s.type == 'bridge' && L.toArray(s.ifname).indexOf(dev.getName()) > -1)
+ isPort = true;
+ });
+
+ return isPort;
+}
+
+function updateDevBadge(node, dev) {
+ var type = dev.getType(),
+ up = dev.getCarrier();
+
+ dom.content(node, [
+ E('img', {
+ 'class': 'middle',
+ 'src': L.resource('icons/%s%s.png').format(type, up ? '' : '_disabled')
+ }),
+ '\x0a', dev.getName()
+ ]);
+
+ return node;
+}
+
+function renderDevBadge(dev) {
+ return updateDevBadge(E('span', {
+ 'class': 'ifacebadge port-status-device',
+ 'style': 'font-weight:normal',
+ 'data-device': dev.getName()
+ }), dev);
+}
+
+function updatePortStatus(node, dev) {
+ var carrier = dev.getCarrier(),
+ duplex = dev.getDuplex(),
+ speed = dev.getSpeed(),
+ desc, title;
+
+ if (carrier && speed > 0 && duplex != null) {
+ desc = '%d%s'.format(speed, duplex == 'full' ? 'FD' : 'HD');
+ title = '%s, %d MBit/s, %s'.format(_('Connected'), speed, duplex == 'full' ? _('full-duplex') : _('half-duplex'));
+ }
+ else if (carrier) {
+ desc = _('Connected');
+ }
+ else {
+ desc = _('no link');
+ }
+
+ dom.content(node, [
+ E('img', {
+ 'class': 'middle',
+ 'src': L.resource('icons/port_%s.png').format(carrier ? 'up' : 'down')
+ }),
+ '\x0a', desc
+ ]);
+
+ if (title)
+ node.setAttribute('data-tooltip', title);
+ else
+ node.removeAttribute('data-tooltip');
+
+ return node;
+}
+
+function renderPortStatus(dev) {
+ return updatePortStatus(E('span', {
+ 'class': 'ifacebadge port-status-link',
+ 'data-device': dev.getName()
+ }), dev);
+}
+
+function updatePlaceholders(opt, section_id) {
+ var dev = network.instantiateDevice(opt.getUIElement(section_id).getValue());
+
+ for (var i = 0, co; (co = opt.section.children[i]) != null; i++) {
+ if (co !== opt) {
+ switch (co.option) {
+ case 'mtu':
+ case 'mtu6':
+ co.getUIElement(section_id).setPlaceholder(dev.getMTU());
+ break;
+
+ case 'macaddr':
+ co.getUIElement(section_id).setPlaceholder(dev.getMAC());
+ break;
+
+ case 'txqueuelen':
+ co.getUIElement(section_id).setPlaceholder(dev._devstate('qlen'));
+ break;
+ }
+ }
+ }
+}
+
+var cbiFlagTristate = form.ListValue.extend({
+ __init__: function(/* ... */) {
+ this.super('__init__', arguments);
+ this.keylist = [ '', '0!', '1!' ];
+ this.vallist = [ _('automatic'), _('disabled'), _('enabled') ];
+ },
+
+ load: function(section_id) {
+ var invert = false, sysfs = this.sysfs;
+
+ if (sysfs) {
+ if (sysfs.charAt(0) == '!') {
+ invert = true;
+ sysfs = sysfs.substring(1);
+ }
+
+ return L.resolveDefault(fs.read(sysfs), '').then(L.bind(function(res) {
+ res = (res || '').trim();
+
+ if (res == '0')
+ this.sysfs_default = invert;
+ else if (res == '1')
+ this.sysfs_default = !invert;
+
+ return this.super('load', [section_id]);
+ }, this));
+ }
+
+ return this.super('load', [section_id]);
+ },
+
+ write: function(section_id, formvalue) {
+ if (formvalue == '1!')
+ return this.super('write', [section_id, '1']);
+ else if (formvalue == '0!')
+ return this.super('write', [section_id, '0']);
+ else
+ return this.super('remove', [section_id]);
+ },
+
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var sysdef = this.sysfs_default;
+
+ if (this.sysfs_default !== null) {
+ this.keylist[0] = sysdef ? '1' : '0';
+ this.vallist[0] = sysdef ? _('automatic (enabled)') : _('automatic (disabled)');
+ }
+
+ return this.super('renderWidget', [section_id, option_index, cfgvalue]);
+ }
+});
+
+
+var cbiTagValue = form.Value.extend({
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var widget = new ui.Dropdown(cfgvalue || ['-'], {
+ '-': E([], [
+ E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '—' ]),
+ E('span', { 'class': 'hide-close' }, [ _('Not Member', 'VLAN port state') ])
+ ]),
+ 'u': E([], [
+ E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 'U' ]),
+ E('span', { 'class': 'hide-close' }, [ _('Untagged', 'VLAN port state') ])
+ ]),
+ 't': E([], [
+ E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 'T' ]),
+ E('span', { 'class': 'hide-close' }, [ _('Tagged', 'VLAN port state') ])
+ ]),
+ '*': E([], [
+ E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '*' ]),
+ E('span', { 'class': 'hide-close' }, [ _('Is Primary VLAN', 'VLAN port state') ])
+ ])
+ }, {
+ id: this.cbid(section_id),
+ sort: [ '-', 'u', 't', '*' ],
+ optional: false,
+ multiple: true
+ });
+
+ var field = this;
+
+ widget.toggleItem = function(sb, li, force_state) {
+ var lis = li.parentNode.querySelectorAll('li'),
+ toggle = ui.Dropdown.prototype.toggleItem;
+
+ toggle.apply(this, [sb, li, force_state]);
+
+ if (force_state != null)
+ return;
+
+ switch (li.getAttribute('data-value'))
+ {
+ case '-':
+ if (li.hasAttribute('selected')) {
+ for (var i = 0; i < lis.length; i++) {
+ switch (lis[i].getAttribute('data-value')) {
+ case '-':
+ break;
+
+ case '*':
+ toggle.apply(this, [sb, lis[i], false]);
+ lis[i].setAttribute('unselectable', '');
+ break;
+
+ default:
+ toggle.apply(this, [sb, lis[i], false]);
+ }
+ }
+ }
+ break;
+
+ case 't':
+ case 'u':
+ if (li.hasAttribute('selected')) {
+ for (var i = 0; i < lis.length; i++) {
+ switch (lis[i].getAttribute('data-value')) {
+ case li.getAttribute('data-value'):
+ break;
+
+ case '*':
+ lis[i].removeAttribute('unselectable');
+ break;
+
+ default:
+ toggle.apply(this, [sb, lis[i], false]);
+ }
+ }
+ }
+ else {
+ toggle.apply(this, [sb, li, true]);
+ }
+ break;
+
+ case '*':
+ if (li.hasAttribute('selected')) {
+ var section_ids = field.section.cfgsections();
+
+ for (var i = 0; i < section_ids.length; i++) {
+ var other_widget = field.getUIElement(section_ids[i]),
+ other_value = L.toArray(other_widget.getValue());
+
+ if (other_widget === this)
+ continue;
+
+ var new_value = other_value.filter(function(v) { return v != '*' });
+
+ if (new_value.length == other_value.length)
+ continue;
+
+ other_widget.setValue(new_value);
+ break;
+ }
+ }
+ }
+ };
+
+ var node = widget.render();
+
+ node.style.minWidth = '4em';
+
+ if (cfgvalue == '-')
+ node.querySelector('li[data-value="*"]').setAttribute('unselectable', '');
+
+ return E('div', { 'style': 'display:inline-block' }, node);
+ },
+
+ cfgvalue: function(section_id) {
+ var ports = L.toArray(uci.get('network', section_id, 'ports'));
+
+ for (var i = 0; i < ports.length; i++) {
+ var s = ports[i].split(/:/);
+
+ if (s[0] != this.port)
+ continue;
+
+ var t = /t/.test(s[1] || '') ? 't' : 'u';
+
+ return /\x2a/.test(s[1] || '') ? [t, '*'] : [t];
+ }
+
+ return ['-'];
+ },
+
+ write: function(section_id, value) {
+ var ports = [];
+
+ for (var i = 0; i < this.section.children.length; i++) {
+ var opt = this.section.children[i];
+
+ if (opt.port) {
+ var val = L.toArray(opt.formvalue(section_id)).join('');
+
+ switch (val) {
+ case '-':
+ break;
+
+ case 'u':
+ ports.push(opt.port);
+ break;
+
+ default:
+ ports.push('%s:%s'.format(opt.port, val));
+ break;
+ }
+ }
+ }
+
+ uci.set('network', section_id, 'ports', ports.length ? ports : null);
+ },
+
+ remove: function() {}
+});
+
+return baseclass.extend({
+ replaceOption: function(s, tabName, optionClass, optionName, optionTitle, optionDescription) {
+ var o = s.getOption(optionName);
+
+ if (o) {
+ if (o.tab) {
+ s.tabs[o.tab].children = s.tabs[o.tab].children.filter(function(opt) {
+ return opt.option != optionName;
+ });
+ }
+
+ s.children = s.children.filter(function(opt) {
+ return opt.option != optionName;
+ });
+ }
+
+ return s.taboption(tabName, optionClass, optionName, optionTitle, optionDescription);
+ },
+
+ addDeviceOptions: function(s, dev, isNew) {
+ var parent_dev = dev ? dev.getParent() : null,
+ devname = dev ? dev.getName() : null,
+ o, ss;
+
+ s.tab('devgeneral', _('General device options'));
+ s.tab('devadvanced', _('Advanced device options'));
+ s.tab('brport', _('Bridge port specific options'));
+ s.tab('bridgevlan', _('Bridge VLAN filtering'));
+
+ o = this.replaceOption(s, 'devgeneral', form.ListValue, 'type', _('Device type'));
+ o.readonly = !isNew;
+ o.value('', _('Network device'));
+ o.value('bridge', _('Bridge device'));
+ o.value('8021q', _('VLAN (802.1q)'));
+ o.value('8021ad', _('VLAN (802.1ad)'));
+ o.value('macvlan', _('MAC VLAN'));
+ o.value('veth', _('Virtual Ethernet'));
+ o.validate = function(section_id, value) {
+ if (value == 'bridge' || value == 'veth')
+ updatePlaceholders(this.section.getOption('name_complex'), section_id);
+
+ return true;
+ };
+
+ o = this.replaceOption(s, 'devgeneral', widgets.DeviceSelect, 'name_simple', _('Existing device'));
+ o.readonly = !isNew;
+ o.rmempty = false;
+ o.noaliases = true;
+ o.default = (dev ? dev.getName() : '');
+ o.ucioption = 'name';
+ o.filter = function(section_id, value) {
+ var dev = network.instantiateDevice(value);
+ return !deviceSectionExists(section_id, value) && (dev.getType() != 'wifi' || dev.isUp());
+ };
+ o.validate = function(section_id, value) {
+ updatePlaceholders(this, section_id);
+
+ return deviceSectionExists(section_id, value)
+ ? _('A configuration for the device "%s" already exists').format(value) : true;
+ };
+ o.onchange = function(ev, section_id, values) {
+ updatePlaceholders(this, section_id);
+ };
+ o.depends('type', '');
+
+ o = this.replaceOption(s, 'devgeneral', widgets.DeviceSelect, 'ifname_single', _('Base device'));
+ o.readonly = !isNew;
+ o.rmempty = false;
+ o.noaliases = true;
+ o.default = (dev ? dev.getName() : '').match(/^.+\.\d+$/) ? dev.getName().replace(/\.\d+$/, '') : '';
+ o.ucioption = 'ifname';
+ o.filter = function(section_id, value) {
+ var dev = network.instantiateDevice(value);
+ return (dev.getType() != 'wifi' || dev.isUp());
+ };
+ o.validate = function(section_id, value) {
+ updatePlaceholders(this, section_id);
+
+ if (isNew) {
+ var type = this.section.formvalue(section_id, 'type'),
+ name = this.section.getUIElement(section_id, 'name_complex');
+
+ if (type == 'macvlan' && value && name && !name.isChanged()) {
+ var i = 0;
+
+ while (deviceSectionExists(section_id, '%smac%d'.format(value, i)))
+ i++;
+
+ name.setValue('%smac%d'.format(value, i));
+ name.triggerValidation();
+ }
+ }
+
+ return true;
+ };
+ o.onchange = function(ev, section_id, values) {
+ updatePlaceholders(this, section_id);
+ };
+ o.depends('type', '8021q');
+ o.depends('type', '8021ad');
+ o.depends('type', 'macvlan');
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'vid', _('VLAN ID'));
+ o.readonly = !isNew;
+ o.datatype = 'range(1, 4094)';
+ o.rmempty = false;
+ o.default = (dev ? dev.getName() : '').match(/^.+\.\d+$/) ? dev.getName().replace(/^.+\./, '') : '';
+ o.validate = function(section_id, value) {
+ var base = this.section.formvalue(section_id, 'ifname_single'),
+ vid = this.section.formvalue(section_id, 'vid'),
+ name = this.section.getUIElement(section_id, 'name_complex');
+
+ if (base && vid && name && !name.isChanged() && isNew) {
+ name.setValue('%s.%d'.format(base, vid));
+ name.triggerValidation();
+ }
+
+ return true;
+ };
+ o.depends('type', '8021q');
+ o.depends('type', '8021ad');
+
+ o = this.replaceOption(s, 'devgeneral', form.ListValue, 'mode', _('Mode'));
+ o.value('vepa', _('VEPA (Virtual Ethernet Port Aggregator)', 'MACVLAN mode'));
+ o.value('private', _('Private (Prevent communication between MAC VLANs)', 'MACVLAN mode'));
+ o.value('bridge', _('Bridge (Support direct communication between MAC VLANs)', 'MACVLAN mode'));
+ o.value('passthru', _('Pass-through (Mirror physical device to single MAC VLAN)', 'MACVLAN mode'));
+ o.depends('type', 'macvlan');
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'name_complex', _('Device name'));
+ o.rmempty = false;
+ o.datatype = 'maxlength(15)';
+ o.readonly = !isNew;
+ o.ucioption = 'name';
+ o.validate = function(section_id, value) {
+ var dev = network.instantiateDevice(value);
+
+ if (deviceSectionExists(section_id, value) || (isNew && (dev.dev || {}).idx))
+ return _('The device name "%s" is already taken').format(value);
+
+ return true;
+ };
+ o.depends({ type: '', '!reverse': true });
+
+ o = this.replaceOption(s, 'devadvanced', form.DynamicList, 'ingress_qos_mapping', _('Ingress QoS mapping'), _('Defines a mapping of VLAN header priority to the Linux internal packet priority on incoming frames'));
+ o.rmempty = true;
+ o.validate = validateQoSMap;
+ o.depends('type', '8021q');
+ o.depends('type', '8021ad');
+
+ o = this.replaceOption(s, 'devadvanced', form.DynamicList, 'egress_qos_mapping', _('Egress QoS mapping'), _('Defines a mapping of Linux internal packet priority to VLAN header priority but for outgoing frames'));
+ o.rmempty = true;
+ o.validate = validateQoSMap;
+ o.depends('type', '8021q');
+ o.depends('type', '8021ad');
+
+ o = this.replaceOption(s, 'devgeneral', widgets.DeviceSelect, 'ifname_multi', _('Bridge ports'));
+ o.size = 10;
+ o.rmempty = true;
+ o.multiple = true;
+ o.noaliases = true;
+ o.nobridges = true;
+ o.ucioption = 'ports';
+ o.default = L.toArray(dev ? dev.getPorts() : null).filter(function(p) { return p.getType() != 'wifi' }).map(function(p) { return p.getName() });
+ o.filter = function(section_id, device_name) {
+ var bridge_name = uci.get('network', section_id, 'name'),
+ choice_dev = network.instantiateDevice(device_name),
+ parent_dev = choice_dev.getParent();
+
+ /* only show wifi networks which are already present in "option ifname" */
+ if (choice_dev.getType() == 'wifi') {
+ var ifnames = L.toArray(uci.get('network', section_id, 'ports'));
+
+ for (var i = 0; i < ifnames.length; i++)
+ if (ifnames[i] == device_name)
+ return true;
+
+ return false;
+ }
+
+ return (!parent_dev || parent_dev.getName() != bridge_name);
+ };
+ o.description = _('Specifies the wired ports to attach to this bridge. In order to attach wireless networks, choose the associated interface as network in the wireless settings.')
+ o.onchange = function(ev, section_id, values) {
+ ss.updatePorts(values);
+
+ return ss.parse().then(function() {
+ ss.redraw();
+ });
+ };
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devgeneral', form.Flag, 'bridge_empty', _('Bring up empty bridge'), _('Bring up the bridge interface even if no ports are attached'));
+ o.default = o.disabled;
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'priority', _('Priority'));
+ o.placeholder = '32767';
+ o.datatype = 'range(0, 65535)';
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'ageing_time', _('Ageing time'), _('Timeout in seconds for learned MAC addresses in the forwarding database'));
+ o.placeholder = '30';
+ o.datatype = 'uinteger';
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devadvanced', form.Flag, 'stp', _('Enable <abbr title="Spanning Tree Protocol">STP</abbr>'), _('Enables the Spanning Tree Protocol on this bridge'));
+ o.default = o.disabled;
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'hello_time', _('Hello interval'), _('Interval in seconds for STP hello packets'));
+ o.placeholder = '2';
+ o.datatype = 'range(1, 10)';
+ o.depends({ type: 'bridge', stp: '1' });
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'forward_delay', _('Forward delay'), _('Time in seconds to spend in listening and learning states'));
+ o.placeholder = '15';
+ o.datatype = 'range(2, 30)';
+ o.depends({ type: 'bridge', stp: '1' });
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'max_age', _('Maximum age'), _('Timeout in seconds until topology updates on link loss'));
+ o.placeholder = '20';
+ o.datatype = 'range(6, 40)';
+ o.depends({ type: 'bridge', stp: '1' });
+
+
+ o = this.replaceOption(s, 'devadvanced', form.Flag, 'igmp_snooping', _('Enable <abbr title="Internet Group Management Protocol">IGMP</abbr> snooping'), _('Enables IGMP snooping on this bridge'));
+ o.default = o.disabled;
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'hash_max', _('Maximum snooping table size'));
+ o.placeholder = '512';
+ o.datatype = 'uinteger';
+ o.depends({ type: 'bridge', igmp_snooping: '1' });
+
+ o = this.replaceOption(s, 'devadvanced', form.Flag, 'multicast_querier', _('Enable multicast querier'));
+ o.defaults = { '1': [{'igmp_snooping': '1'}], '0': [{'igmp_snooping': '0'}] };
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'robustness', _('Robustness'), _('The robustness value allows tuning for the expected packet loss on the network. If a network is expected to be lossy, the robustness value may be increased. IGMP is robust to (Robustness-1) packet losses'));
+ o.placeholder = '2';
+ o.datatype = 'min(1)';
+ o.depends({ type: 'bridge', multicast_querier: '1' });
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'query_interval', _('Query interval'), _('Interval in centiseconds between multicast general queries. By varying the value, an administrator may tune the number of IGMP messages on the subnet; larger values cause IGMP Queries to be sent less often'));
+ o.placeholder = '12500';
+ o.datatype = 'uinteger';
+ o.depends({ type: 'bridge', multicast_querier: '1' });
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'query_response_interval', _('Query response interval'), _('The max response time in centiseconds inserted into the periodic general queries. By varying the value, an administrator may tune the burstiness of IGMP messages on the subnet; larger values make the traffic less bursty, as host responses are spread out over a larger interval'));
+ o.placeholder = '1000';
+ o.datatype = 'uinteger';
+ o.validate = function(section_id, value) {
+ var qiopt = L.toArray(this.map.lookupOption('query_interval', section_id))[0],
+ qival = qiopt ? (qiopt.formvalue(section_id) || qiopt.placeholder) : '';
+
+ if (value != '' && qival != '' && +value >= +qival)
+ return _('The query response interval must be lower than the query interval value');
+
+ return true;
+ };
+ o.depends({ type: 'bridge', multicast_querier: '1' });
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'last_member_interval', _('Last member interval'), _('The max response time in centiseconds inserted into group-specific queries sent in response to leave group messages. It is also the amount of time between group-specific query messages. This value may be tuned to modify the "leave latency" of the network. A reduced value results in reduced time to detect the loss of the last member of a group'));
+ o.placeholder = '100';
+ o.datatype = 'uinteger';
+ o.depends({ type: 'bridge', multicast_querier: '1' });
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'mtu', _('MTU'));
+ o.datatype = 'range(576, 9200)';
+ o.validate = function(section_id, value) {
+ var parent_mtu = (dev && dev.getType() == 'vlan') ? (parent_dev ? parent_dev.getMTU() : null) : null;
+
+ if (parent_mtu !== null && +value > parent_mtu)
+ return _('The MTU must not exceed the parent device MTU of %d bytes').format(parent_mtu);
+
+ return true;
+ };
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'macaddr', _('MAC address'));
+ o.datatype = 'macaddr';
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'peer_name', _('Peer device name'));
+ o.rmempty = true;
+ o.datatype = 'maxlength(15)';
+ o.depends('type', 'veth');
+ o.load = function(section_id) {
+ var sections = uci.sections('network', 'device'),
+ idx = 0;
+
+ for (var i = 0; i < sections.length; i++)
+ if (sections[i]['.name'] == section_id)
+ break;
+ else if (sections[i].type == 'veth')
+ idx++;
+
+ this.placeholder = 'veth%d'.format(idx);
+
+ return form.Value.prototype.load.apply(this, arguments);
+ };
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'peer_macaddr', _('Peer MAC address'));
+ o.rmempty = true;
+ o.datatype = 'macaddr';
+ o.depends('type', 'veth');
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'txqueuelen', _('TX queue length'));
+ o.placeholder = dev ? dev._devstate('qlen') : '';
+ o.datatype = 'uinteger';
+
+ o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'promisc', _('Enable promiscuous mode'));
+ o.sysfs_default = (dev && dev.dev && dev.dev.flags) ? dev.dev.flags.promisc : null;
+
+ o = this.replaceOption(s, 'devadvanced', form.ListValue, 'rpfilter', _('Reverse path filter'));
+ o.default = '';
+ o.value('', _('disabled'));
+ o.value('loose', _('Loose filtering'));
+ o.value('strict', _('Strict filtering'));
+ o.cfgvalue = function(/* ... */) {
+ var val = form.ListValue.prototype.cfgvalue.apply(this, arguments);
+
+ switch (val || '') {
+ case 'loose':
+ case '1':
+ return 'loose';
+
+ case 'strict':
+ case '2':
+ return 'strict';
+
+ default:
+ return '';
+ }
+ };
+
+ o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'acceptlocal', _('Accept local'), _('Accept packets with local source addresses'));
+ o.sysfs = '/proc/sys/net/ipv4/conf/%s/accept_local'.format(devname || 'default');
+
+ o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'sendredirects', _('Send ICMP redirects'));
+ o.sysfs = '/proc/sys/net/ipv4/conf/%s/send_redirects'.format(devname || 'default');
+
+ o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'arp_accept ', _('Honor gratuitous ARP'), _('When enabled, new ARP table entries are added from received gratuitous APR requests or replies, otherwise only preexisting table entries are updated, but no new hosts are learned.'));
+ o.sysfs = '/proc/sys/net/ipv4/conf/%s/arp_accept'.format(devname || 'default');
+
+ o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'drop_gratuitous_arp', _('Drop gratuitous ARP'), _('Drop all gratuitous ARP frames, for example if there’s a known good ARP proxy on the network and such frames need not be used or in the case of 802.11, must not be used to prevent attacks.'));
+ o.sysfs = '/proc/sys/net/ipv4/conf/%s/drop_gratuitous_arp'.format(devname || 'default');
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'neighreachabletime', _('Neighbour cache validity'), _('Time in milliseconds'));
+ o.placeholder = '30000';
+ o.datatype = 'uinteger';
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'neighgcstaletime', _('Stale neighbour cache timeout'), _('Timeout in seconds'));
+ o.placeholder = '60';
+ o.datatype = 'uinteger';
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'neighlocktime', _('Minimum ARP validity time'), _('Minimum required time in seconds before an ARP entry may be replaced. Prevents ARP cache thrashing.'));
+ o.placeholder = '0';
+ o.datatype = 'uinteger';
+
+ o = this.replaceOption(s, 'devgeneral', cbiFlagTristate, 'ipv6', _('Enable IPv6'));
+ o.sysfs = '!/proc/sys/net/ipv6/conf/%s/disable_ipv6'.format(devname || 'default');
+ o.migrate = false;
+
+ o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'ip6segmentrouting', _('Enable IPv6 segment routing'));
+ o.sysfs = '/proc/sys/net/ipv6/conf/%s/seg6_enabled'.format(devname || 'default');
+ o.depends('ipv6', /1/);
+
+ o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'drop_unsolicited_na', _('Drop unsolicited NA'), _('Drop all unsolicited neighbor advertisements, for example if there’s a known good NA proxy on the network and such frames need not be used or in the case of 802.11, must not be used to prevent attacks.'));
+ o.sysfs = '/proc/sys/net/ipv6/conf/%s/drop_unsolicited_na'.format(devname || 'default');
+ o.depends('ipv6', /1/);
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'mtu6', _('IPv6 MTU'));
+ o.datatype = 'max(9200)';
+ o.depends('ipv6', /1/);
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'dadtransmits', _('DAD transmits'), _('Amount of Duplicate Address Detection probes to send'));
+ o.placeholder = '1';
+ o.datatype = 'uinteger';
+ o.depends('ipv6', /1/);
+
+
+ o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'multicast', _('Enable multicast support'));
+ o.sysfs_default = (dev && dev.dev && dev.dev.flags) ? dev.dev.flags.multicast : null;
+
+ o = this.replaceOption(s, 'devadvanced', form.ListValue, 'igmpversion', _('Force IGMP version'));
+ o.value('', _('No enforcement'));
+ o.value('1', _('Enforce IGMPv1'));
+ o.value('2', _('Enforce IGMPv2'));
+ o.value('3', _('Enforce IGMPv3'));
+ o.depends('multicast', /1/);
+
+ o = this.replaceOption(s, 'devadvanced', form.ListValue, 'mldversion', _('Force MLD version'));
+ o.value('', _('No enforcement'));
+ o.value('1', _('Enforce MLD version 1'));
+ o.value('2', _('Enforce MLD version 2'));
+ o.depends('multicast', /1/);
+
+ if (isBridgePort(dev)) {
+ o = this.replaceOption(s, 'brport', cbiFlagTristate, 'learning', _('Enable MAC address learning'));
+ o.sysfs = '/sys/class/net/%s/brport/learning'.format(devname || 'default');
+
+ o = this.replaceOption(s, 'brport', cbiFlagTristate, 'unicast_flood', _('Enable unicast flooding'));
+ o.sysfs = '/sys/class/net/%s/brport/unicast_flood'.format(devname || 'default');
+
+ o = this.replaceOption(s, 'brport', cbiFlagTristate, 'isolate', _('Port isolation'), _('Only allow communication with non-isolated bridge ports when enabled'));
+ o.sysfs = '/sys/class/net/%s/brport/isolated'.format(devname || 'default');
+
+ o = this.replaceOption(s, 'brport', form.ListValue, 'multicast_router', _('Multicast routing'));
+ o.value('', _('Never'));
+ o.value('1', _('Learn'));
+ o.value('2', _('Always'));
+ o.depends('multicast', /1/);
+
+ o = this.replaceOption(s, 'brport', cbiFlagTristate, 'multicast_to_unicast', _('Multicast to unicast'), _('Forward multicast packets as unicast packets on this device.'));
+ o.sysfs = '/sys/class/net/%s/brport/multicast_to_unicast'.format(devname || 'default');
+ o.depends('multicast', /1/);
+
+ o = this.replaceOption(s, 'brport', cbiFlagTristate, 'multicast_fast_leave', _('Enable multicast fast leave'));
+ o.sysfs = '/sys/class/net/%s/brport/multicast_fast_leave'.format(devname || 'default');
+ o.depends('multicast', /1/);
+
+ o = this.replaceOption(s, 'brport', cbiFlagTristate, 'drop_v4_unicast_in_l2_multicast', _('Drop nested IPv4 unicast'), _('Drop layer 2 multicast frames containing IPv4 unicast packets.'));
+ o.sysfs = '/proc/sys/net/ipv4/conf/%s/drop_unicast_in_l2_multicast'.format(devname || 'default');
+ o.depends('multicast', /1/);
+
+ o = this.replaceOption(s, 'brport', cbiFlagTristate, 'drop_v6_unicast_in_l2_multicast', _('Drop nested IPv6 unicast'), _('Drop layer 2 multicast frames containing IPv6 unicast packets.'));
+ o.sysfs = '/proc/sys/net/ipv6/conf/%s/drop_unicast_in_l2_multicast'.format(devname || 'default');
+ o.depends('multicast', /1/);
+ }
+
+ o = this.replaceOption(s, 'bridgevlan', form.Flag, 'vlan_filtering', _('Enable VLAN filtering'));
+ o.depends('type', 'bridge');
+ o.updateDefaultValue = function(section_id) {
+ var device = uci.get('network', s.section, 'name'),
+ uielem = this.getUIElement(section_id),
+ has_vlans = false;
+
+ uci.sections('network', 'bridge-vlan', function(bvs) {
+ has_vlans = has_vlans || (bvs.device == device);
+ });
+
+ this.default = has_vlans ? this.enabled : this.disabled;
+
+ if (uielem && !uielem.isChanged())
+ uielem.setValue(this.default);
+ };
+
+ o = this.replaceOption(s, 'bridgevlan', form.SectionValue, 'bridge-vlan', form.TableSection, 'bridge-vlan');
+ o.depends('type', 'bridge');
+
+ ss = o.subsection;
+ ss.addremove = true;
+ ss.anonymous = true;
+
+ ss.renderHeaderRows = function(/* ... */) {
+ var node = form.TableSection.prototype.renderHeaderRows.apply(this, arguments);
+
+ node.querySelectorAll('.th').forEach(function(th) {
+ th.classList.add('left');
+ th.classList.add('middle');
+ });
+
+ return node;
+ };
+
+ ss.filter = function(section_id) {
+ var devname = uci.get('network', s.section, 'name');
+ return (uci.get('network', section_id, 'device') == devname);
+ };
+
+ ss.render = function(/* ... */) {
+ return form.TableSection.prototype.render.apply(this, arguments).then(L.bind(function(node) {
+ node.style.overflow = 'auto hidden';
+ node.style.paddingTop = '1em';
+
+ if (this.node)
+ this.node.parentNode.replaceChild(node, this.node);
+
+ this.node = node;
+
+ return node;
+ }, this));
+ };
+
+ ss.redraw = function() {
+ return this.load().then(L.bind(this.render, this));
+ };
+
+ ss.updatePorts = function(ports) {
+ var devices = ports.map(function(port) {
+ return network.instantiateDevice(port)
+ }).filter(function(dev) {
+ return dev.getType() != 'wifi' || dev.isUp();
+ }).sort(function(a, b) {
+ return L.naturalCompare(a.getName(), b.getName());
+ });
+
+ this.children = this.children.filter(function(opt) { return !opt.option.match(/^port_/) });
+
+ for (var i = 0; i < devices.length; i++) {
+ o = ss.option(cbiTagValue, 'port_%s'.format(sfh(devices[i].getName())), renderDevBadge(devices[i]), renderPortStatus(devices[i]));
+ o.port = devices[i].getName();
+ }
+
+ var section_ids = this.cfgsections(),
+ device_names = devices.reduce(function(names, dev) { names[dev.getName()] = true; return names }, {});
+
+ for (var i = 0; i < section_ids.length; i++) {
+ var old_spec = L.toArray(uci.get('network', section_ids[i], 'ports')),
+ new_spec = old_spec.filter(function(spec) { return device_names[spec.replace(/:[ut*]+$/, '')] });
+
+ if (old_spec.length != new_spec.length)
+ uci.set('network', section_ids[i], 'ports', new_spec.length ? new_spec : null);
+ }
+ };
+
+ ss.handleAdd = function(ev) {
+ return s.parse().then(L.bind(function() {
+ var device = uci.get('network', s.section, 'name'),
+ section_ids = this.cfgsections(),
+ section_id = null,
+ max_vlan_id = 0;
+
+ if (!device)
+ return;
+
+ for (var i = 0; i < section_ids.length; i++) {
+ var vid = +uci.get('network', section_ids[i], 'vlan');
+
+ if (vid > max_vlan_id)
+ max_vlan_id = vid;
+ }
+
+ section_id = uci.add('network', 'bridge-vlan');
+ uci.set('network', section_id, 'device', device);
+ uci.set('network', section_id, 'vlan', max_vlan_id + 1);
+
+ s.children.forEach(function(opt) {
+ switch (opt.option) {
+ case 'type':
+ case 'name_complex':
+ var input = opt.map.findElement('id', 'widget.%s'.format(opt.cbid(s.section)));
+ if (input)
+ input.disabled = true;
+ break;
+ }
+ });
+
+ s.getOption('vlan_filtering').updateDefaultValue(s.section);
+
+ s.map.addedVLANs = s.map.addedVLANs || [];
+ s.map.addedVLANs.push(section_id);
+
+ return this.redraw();
+ }, this));
+ };
+
+ o = ss.option(form.Value, 'vlan', _('VLAN ID'));
+ o.datatype = 'range(1, 4094)';
+
+ o.renderWidget = function(/* ... */) {
+ var node = form.Value.prototype.renderWidget.apply(this, arguments);
+
+ node.style.width = '5em';
+
+ return node;
+ };
+
+ o.validate = function(section_id, value) {
+ var section_ids = this.section.cfgsections();
+
+ for (var i = 0; i < section_ids.length; i++) {
+ if (section_ids[i] == section_id)
+ continue;
+
+ if (uci.get('network', section_ids[i], 'vlan') == value)
+ return _('The VLAN ID must be unique');
+ }
+
+ return true;
+ };
+
+ o = ss.option(form.Flag, 'local', _('Local'));
+ o.default = o.enabled;
+
+ var ports = [];
+
+ var seen_ports = {};
+
+ L.toArray(uci.get('network', s.section, 'ports')).forEach(function(port) {
+ seen_ports[port] = true;
+ });
+
+ uci.sections('network', 'bridge-vlan', function(bvs) {
+ if (uci.get('network', s.section, 'name') != bvs.device)
+ return;
+
+ L.toArray(bvs.ports).forEach(function(portspec) {
+ var m = portspec.match(/^([^:]+)(?::[ut*]+)?$/);
+
+ if (m)
+ seen_ports[m[1]] = true;
+ });
+ });
+
+ for (var port_name in seen_ports)
+ ports.push(port_name);
+
+ ss.updatePorts(ports);
+ },
+
+ updateDevBadge: updateDevBadge,
+ updatePortStatus: updatePortStatus
+});
diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js
index fa991db41d..da0eeabb5a 100644
--- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js
+++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js
@@ -5,7 +5,9 @@
'require rpc';
'require uci';
'require form';
+'require network';
'require validation';
+'require tools.widgets as widgets';
var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus, CBILease6Status;
@@ -31,15 +33,15 @@ CBILeaseStatus = form.DummyValue.extend({
renderWidget: function(section_id, option_id, cfgvalue) {
return E([
E('h4', _('Active DHCP Leases')),
- E('div', { 'id': 'lease_status_table', 'class': 'table' }, [
- E('div', { 'class': 'tr table-titles' }, [
- E('div', { 'class': 'th' }, _('Hostname')),
- E('div', { 'class': 'th' }, _('IPv4-Address')),
- E('div', { 'class': 'th' }, _('MAC-Address')),
- E('div', { 'class': 'th' }, _('Lease time remaining'))
+ E('table', { 'id': 'lease_status_table', 'class': 'table' }, [
+ E('tr', { 'class': 'tr table-titles' }, [
+ E('th', { 'class': 'th' }, _('Hostname')),
+ E('th', { 'class': 'th' }, _('IPv4 address')),
+ E('th', { 'class': 'th' }, _('MAC address')),
+ E('th', { 'class': 'th' }, _('Lease time remaining'))
]),
- E('div', { 'class': 'tr placeholder' }, [
- E('div', { 'class': 'td' }, E('em', _('Collecting data...')))
+ E('tr', { 'class': 'tr placeholder' }, [
+ E('td', { 'class': 'td' }, E('em', _('Collecting data...')))
])
])
]);
@@ -50,21 +52,73 @@ CBILease6Status = form.DummyValue.extend({
renderWidget: function(section_id, option_id, cfgvalue) {
return E([
E('h4', _('Active DHCPv6 Leases')),
- E('div', { 'id': 'lease6_status_table', 'class': 'table' }, [
- E('div', { 'class': 'tr table-titles' }, [
- E('div', { 'class': 'th' }, _('Host')),
- E('div', { 'class': 'th' }, _('IPv6-Address')),
- E('div', { 'class': 'th' }, _('DUID')),
- E('div', { 'class': 'th' }, _('Lease time remaining'))
+ E('table', { 'id': 'lease6_status_table', 'class': 'table' }, [
+ E('tr', { 'class': 'tr table-titles' }, [
+ E('th', { 'class': 'th' }, _('Host')),
+ E('th', { 'class': 'th' }, _('IPv6 address')),
+ E('th', { 'class': 'th' }, _('DUID')),
+ E('th', { 'class': 'th' }, _('Lease time remaining'))
]),
- E('div', { 'class': 'tr placeholder' }, [
- E('div', { 'class': 'td' }, E('em', _('Collecting data...')))
+ E('tr', { 'class': 'tr placeholder' }, [
+ E('td', { 'class': 'td' }, E('em', _('Collecting data...')))
])
])
]);
}
});
+function calculateNetwork(addr, mask) {
+ addr = validation.parseIPv4(String(addr));
+
+ if (!isNaN(mask))
+ mask = validation.parseIPv4(network.prefixToMask(+mask));
+ else
+ mask = validation.parseIPv4(String(mask));
+
+ if (addr == null || mask == null)
+ return null;
+
+ return [
+ [
+ addr[0] & (mask[0] >>> 0 & 255),
+ addr[1] & (mask[1] >>> 0 & 255),
+ addr[2] & (mask[2] >>> 0 & 255),
+ addr[3] & (mask[3] >>> 0 & 255)
+ ].join('.'),
+ mask.join('.')
+ ];
+}
+
+function getDHCPPools() {
+ return uci.load('dhcp').then(function() {
+ let sections = uci.sections('dhcp', 'dhcp'),
+ tasks = [], pools = [];
+
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i].ignore == '1' || !sections[i].interface)
+ continue;
+
+ tasks.push(network.getNetwork(sections[i].interface).then(L.bind(function(section_id, net) {
+ var cidr = net ? (net.getIPAddrs()[0] || '').split('/') : null;
+
+ if (cidr && cidr.length == 2) {
+ var net_mask = calculateNetwork(cidr[0], cidr[1]);
+
+ pools.push({
+ section_id: section_id,
+ network: net_mask[0],
+ netmask: net_mask[1]
+ });
+ }
+ }, null, sections[i]['.name'])));
+ }
+
+ return Promise.all(tasks).then(function() {
+ return pools;
+ });
+ });
+}
+
function validateHostname(sid, s) {
if (s == null || s == '')
return true;
@@ -72,7 +126,7 @@ function validateHostname(sid, s) {
if (s.length > 256)
return _('Expecting: %s').format(_('valid hostname'));
- var labels = s.replace(/^\.+|\.$/g, '').split(/\./);
+ var labels = s.replace(/^\*?\.?|\.$/g, '').split(/\./);
for (var i = 0; i < labels.length; i++)
if (!labels[i].match(/^[a-z0-9_](?:[a-z0-9-]{0,61}[a-z0-9])?$/i))
@@ -102,13 +156,15 @@ function validateServerSpec(sid, s) {
if (s == null || s == '')
return true;
- var m = s.match(/^(?:\/(.+)\/)?(.*)$/);
+ var m = s.match(/^(\/.*\/)?(.*)$/);
if (!m)
return _('Expecting: %s').format(_('valid hostname'));
- var res = validateAddressList(sid, m[1]);
- if (res !== true)
- return res;
+ if (m[1] != '//' && m[1] != '/#/') {
+ var res = validateAddressList(sid, m[1]);
+ if (res !== true)
+ return res;
+ }
if (m[2] == '' || m[2] == '#')
return true;
@@ -138,278 +194,596 @@ function validateServerSpec(sid, s) {
return true;
}
+function validateMACAddr(pools, sid, s) {
+ if (s == null || s == '')
+ return true;
+
+ var leases = uci.sections('dhcp', 'host'),
+ this_macs = L.toArray(s).map(function(m) { return m.toUpperCase() });
+
+ for (var i = 0; i < pools.length; i++) {
+ var this_net_mask = calculateNetwork(this.section.formvalue(sid, 'ip'), pools[i].netmask);
+
+ if (!this_net_mask)
+ continue;
+
+ for (var j = 0; j < leases.length; j++) {
+ if (leases[j]['.name'] == sid || !leases[j].ip)
+ continue;
+
+ var lease_net_mask = calculateNetwork(leases[j].ip, pools[i].netmask);
+
+ if (!lease_net_mask || this_net_mask[0] != lease_net_mask[0])
+ continue;
+
+ var lease_macs = L.toArray(leases[j].mac).map(function(m) { return m.toUpperCase() });
+
+ for (var k = 0; k < lease_macs.length; k++)
+ for (var l = 0; l < this_macs.length; l++)
+ if (lease_macs[k] == this_macs[l])
+ return _('The MAC address %h is already used by another static lease in the same DHCP pool').format(this_macs[l]);
+ }
+ }
+
+ return true;
+}
+
return view.extend({
load: function() {
return Promise.all([
callHostHints(),
- callDUIDHints()
+ callDUIDHints(),
+ getDHCPPools(),
+ network.getNetworks()
]);
},
- render: function(hosts_duids) {
+ render: function(hosts_duids_pools) {
var has_dhcpv6 = L.hasSystemFeature('dnsmasq', 'dhcpv6') || L.hasSystemFeature('odhcpd'),
- hosts = hosts_duids[0],
- duids = hosts_duids[1],
+ hosts = hosts_duids_pools[0],
+ duids = hosts_duids_pools[1],
+ pools = hosts_duids_pools[2],
+ networks = hosts_duids_pools[3],
m, s, o, ss, so;
- m = new form.Map('dhcp', _('DHCP and DNS'), _('Dnsmasq is a combined <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-Server and <abbr title="Domain Name System">DNS</abbr>-Forwarder for <abbr title="Network Address Translation">NAT</abbr> firewalls'));
+ m = new form.Map('dhcp', _('DHCP and DNS'),
+ _('Dnsmasq is a lightweight <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> server and <abbr title="Domain Name System">DNS</abbr> forwarder.'));
- s = m.section(form.TypedSection, 'dnsmasq', _('Server Settings'));
+ s = m.section(form.TypedSection, 'dnsmasq');
s.anonymous = true;
s.addremove = false;
s.tab('general', _('General Settings'));
- s.tab('files', _('Resolv and Hosts Files'));
- s.tab('tftp', _('TFTP Settings'));
s.tab('advanced', _('Advanced Settings'));
s.tab('leases', _('Static Leases'));
+ s.tab('files', _('Resolv and Hosts Files'));
+ s.tab('hosts', _('Hostnames'));
+ s.tab('ipsets', _('IP Sets'));
+ s.tab('relay', _('Relay'));
+ s.tab('srvhosts', _('SRV'));
+ s.tab('mxhosts', _('MX'));
+ s.tab('cnamehosts', _('CNAME'));
+ s.tab('pxe_tftp', _('PXE/TFTP Settings'));
s.taboption('general', form.Flag, 'domainneeded',
_('Domain required'),
- _('Don\'t forward <abbr title="Domain Name System">DNS</abbr>-Requests without <abbr title="Domain Name System">DNS</abbr>-Name'));
+ _('Do not forward DNS queries without dots or domain parts.'));
s.taboption('general', form.Flag, 'authoritative',
_('Authoritative'),
- _('This is the only <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> in the local network'));
+ _('This is the only DHCP server in the local network.'));
+ s.taboption('general', form.Value, 'local',
+ _('Local server'),
+ _('Never forward matching domains and subdomains, resolve from DHCP or hosts files only.'));
+
+ s.taboption('general', form.Value, 'domain',
+ _('Local domain'),
+ _('Local domain suffix appended to DHCP names and hosts file entries.'));
+
+ o = s.taboption('general', form.Flag, 'logqueries',
+ _('Log queries'),
+ _('Write received DNS queries to syslog.'));
+ o.optional = true;
+
+ o = s.taboption('general', form.DynamicList, 'server',
+ _('DNS forwardings'),
+ _('List of upstream resolvers to forward queries to.'));
+ o.optional = true;
+ o.placeholder = '/example.org/10.1.2.3';
+ o.validate = validateServerSpec;
+
+ o = s.taboption('general', form.DynamicList, 'address',
+ _('Addresses'),
+ _('Resolve specified FQDNs to an IP.') + '<br />' +
+ _('Syntax: <code>/fqdn[/fqdn…]/[ipaddr]</code>.') + '<br />' +
+ _('<code>/#/</code> matches any domain. <code>/example.com/</code> returns NXDOMAIN.') + '<br />' +
+ _('<code>/example.com/#</code> returns NULL addresses (<code>0.0.0.0</code> and <code>::</code>) for example.com and its subdomains.'));
+ o.optional = true;
+ o.placeholder = '/router.local/router.lan/192.168.0.1';
+
+ o = s.taboption('general', form.DynamicList, 'ipset',
+ _('IP sets'),
+ _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.'));
+ o.optional = true;
+ o.placeholder = '/example.org/ipset,ipset6';
+
+ o = s.taboption('general', form.Flag, 'rebind_protection',
+ _('Rebind protection'),
+ _('Discard upstream responses containing <a href="%s">RFC1918</a> addresses.').format('https://datatracker.ietf.org/doc/html/rfc1918'));
+ o.rmempty = false;
+
+ o = s.taboption('general', form.Flag, 'rebind_localhost',
+ _('Allow localhost'),
+ _('Exempt <code>127.0.0.0/8</code> and <code>::1</code> from rebinding checks, e.g. for RBL services.'));
+ o.depends('rebind_protection', '1');
+
+ o = s.taboption('general', form.DynamicList, 'rebind_domain',
+ _('Domain whitelist'),
+ _('List of domains to allow RFC1918 responses for.'));
+ o.depends('rebind_protection', '1');
+ o.optional = true;
+ o.placeholder = 'ihost.netflix.com';
+ o.validate = validateAddressList;
+
+ o = s.taboption('general', form.Flag, 'localservice',
+ _('Local service only'),
+ _('Accept DNS queries only from hosts whose address is on a local subnet.'));
+ o.optional = false;
+ o.rmempty = false;
+
+ o = s.taboption('general', form.Flag, 'nonwildcard',
+ _('Non-wildcard'),
+ _('Bind dynamically to interfaces rather than wildcard address.'));
+ o.default = o.enabled;
+ o.optional = false;
+ o.rmempty = true;
+
+ o = s.taboption('general', form.DynamicList, 'interface',
+ _('Listen interfaces'),
+ _('Listen only on the specified interfaces, and loopback if not excluded explicitly.'));
+ o.optional = true;
+ o.placeholder = 'lan';
+
+ o = s.taboption('general', form.DynamicList, 'notinterface',
+ _('Exclude interfaces'),
+ _('Do not listen on the specified interfaces.'));
+ o.optional = true;
+ o.placeholder = 'loopback';
+
+ o = s.taboption('relay', form.SectionValue, '__relays__', form.TableSection, 'relay', null,
+ _('Relay DHCP requests elsewhere. OK: v4↔v4, v6↔v6. Not OK: v4↔v6, v6↔v4.')
+ + '<br />' + _('Note: you may also need a DHCP Proxy (currently unavailable) when specifying a non-standard Relay To port(<code>addr#port</code>).')
+ + '<br />' + _('You may add multiple unique Relay To on the same Listen addr.'));
+
+ ss = o.subsection;
+
+ ss.addremove = true;
+ ss.anonymous = true;
+ ss.sortable = true;
+ ss.rowcolors = true;
+ ss.nodescriptions = true;
+
+ so = ss.option(form.Value, 'local_addr', _('Relay from'));
+ so.rmempty = false;
+ so.datatype = 'ipaddr';
+
+ for (var family = 4; family <= 6; family += 2) {
+ for (var i = 0; i < networks.length; i++) {
+ if (networks[i].getName() != 'loopback') {
+ var addrs = (family == 6) ? networks[i].getIP6Addrs() : networks[i].getIPAddrs();
+ for (var j = 0; j < addrs.length; j++) {
+ var addr = addrs[j].split('/')[0];
+ so.value(addr, E([], [
+ addr, ' (',
+ widgets.NetworkSelect.prototype.renderIfaceBadge(networks[i]),
+ ')'
+ ]));
+ }
+ }
+ }
+ }
+
+ so = ss.option(form.Value, 'server_addr', _('Relay to address'));
+ so.rmempty = false;
+ so.optional = false;
+ so.placeholder = '192.168.10.1#535';
+
+ so.validate = function(section, value) {
+ var m = this.section.formvalue(section, 'local_addr'),
+ n = this.section.formvalue(section, 'server_addr'),
+ p;
+ if (n != null && n != '')
+ p = n.split('#');
+ if (p.length > 1 && !/^[0-9]+$/.test(p[1]))
+ return _('Expected port number.');
+ else
+ n = p[0];
+
+ if ((m == null || m == '') && (n == null || n == ''))
+ return _('Both "Relay from" and "Relay to address" must be specified.');
+
+ if ((validation.parseIPv6(m) && validation.parseIPv6(n)) ||
+ validation.parseIPv4(m) && validation.parseIPv4(n))
+ return true;
+ else
+ return _('Address families of "Relay from" and "Relay to address" must match.')
+ };
+
+ so = ss.option(widgets.NetworkSelect, 'interface', _('Only accept replies via'));
+ so.optional = true;
+ so.rmempty = false;
+ so.placeholder = 'lan';
s.taboption('files', form.Flag, 'readethers',
_('Use <code>/etc/ethers</code>'),
- _('Read <code>/etc/ethers</code> to configure the <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-Server'));
+ _('Read <code>/etc/ethers</code> to configure the DHCP server.'));
s.taboption('files', form.Value, 'leasefile',
- _('Leasefile'),
- _('file where given <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-leases will be stored'));
+ _('Lease file'),
+ _('File to store DHCP lease information.'));
- s.taboption('files', form.Flag, 'noresolv',
- _('Ignore resolve file')).optional = true;
+ o = s.taboption('files', form.Flag, 'noresolv',
+ _('Ignore resolv file'));
+ o.optional = true;
o = s.taboption('files', form.Value, 'resolvfile',
- _('Resolve file'),
- _('local <abbr title="Domain Name System">DNS</abbr> file'));
-
+ _('Resolv file'),
+ _('File with upstream resolvers.'));
o.depends('noresolv', '0');
o.placeholder = '/tmp/resolv.conf.d/resolv.conf.auto';
o.optional = true;
+ o = s.taboption('files', form.Flag, 'nohosts',
+ _('Ignore <code>/etc/hosts</code>'));
+ o.optional = true;
- s.taboption('files', form.Flag, 'nohosts',
- _('Ignore <code>/etc/hosts</code>')).optional = true;
-
- s.taboption('files', form.DynamicList, 'addnhosts',
- _('Additional Hosts files')).optional = true;
+ o = s.taboption('files', form.DynamicList, 'addnhosts',
+ _('Additional hosts files'));
+ o.optional = true;
+ o.placeholder = '/etc/dnsmasq.hosts';
o = s.taboption('advanced', form.Flag, 'quietdhcp',
_('Suppress logging'),
- _('Suppress logging of the routine operation of these protocols'));
+ _('Suppress logging of the routine operation for the DHCP protocol.'));
o.optional = true;
o = s.taboption('advanced', form.Flag, 'sequential_ip',
- _('Allocate IP sequentially'),
- _('Allocate IP addresses sequentially, starting from the lowest available address'));
+ _('Allocate IPs sequentially'),
+ _('Allocate IP addresses sequentially, starting from the lowest available address.'));
o.optional = true;
o = s.taboption('advanced', form.Flag, 'boguspriv',
_('Filter private'),
- _('Do not forward reverse lookups for local networks'));
+ _('Do not forward reverse lookups for local networks.'));
o.default = o.enabled;
s.taboption('advanced', form.Flag, 'filterwin2k',
- _('Filter useless'),
- _('Do not forward requests that cannot be answered by public name servers'));
+ _('Filter SRV/SOA service discovery'),
+ _('Filters SRV/SOA service discovery, to avoid triggering dial-on-demand links.') + '<br />' +
+ _('May prevent VoIP or other services from working.'));
+
+ o = s.taboption('advanced', form.Flag, 'filter_aaaa',
+ _('Filter IPv6 AAAA records'),
+ _('Remove IPv6 addresses from the results and only return IPv4 addresses.') + '<br />' +
+ _('Can be useful if ISP has IPv6 nameservers but does not provide IPv6 routing.'));
+ o.optional = true;
+ o = s.taboption('advanced', form.Flag, 'filter_a',
+ _('Filter IPv4 A records'),
+ _('Remove IPv4 addresses from the results and only return IPv6 addresses.'));
+ o.optional = true;
s.taboption('advanced', form.Flag, 'localise_queries',
_('Localise queries'),
- _('Localise hostname depending on the requesting subnet if multiple IPs are available'));
+ _('Return answers to DNS queries matching the subnet from which the query was received if multiple IPs are available.'));
if (L.hasSystemFeature('dnsmasq', 'dnssec')) {
o = s.taboption('advanced', form.Flag, 'dnssec',
- _('DNSSEC'));
+ _('DNSSEC'),
+ _('Validate DNS replies and cache DNSSEC data, requires upstream to support DNSSEC.'));
o.optional = true;
o = s.taboption('advanced', form.Flag, 'dnsseccheckunsigned',
_('DNSSEC check unsigned'),
- _('Requires upstream supports DNSSEC; verify unsigned domain responses really come from unsigned domains'));
+ _('Verify unsigned domain responses really come from unsigned domains.'));
o.default = o.enabled;
o.optional = true;
}
- s.taboption('general', form.Value, 'local',
- _('Local server'),
- _('Local domain specification. Names matching this domain are never forwarded and are resolved from DHCP or hosts files only'));
-
- s.taboption('general', form.Value, 'domain',
- _('Local domain'),
- _('Local domain suffix appended to DHCP names and hosts file entries'));
-
s.taboption('advanced', form.Flag, 'expandhosts',
_('Expand hosts'),
- _('Add local domain suffix to names served from hosts files'));
+ _('Add local domain suffix to names served from hosts files.'));
s.taboption('advanced', form.Flag, 'nonegcache',
_('No negative cache'),
- _('Do not cache negative replies, e.g. for not existing domains'));
+ _('Do not cache negative replies, e.g. for non-existent domains.'));
- s.taboption('advanced', form.Value, 'serversfile',
+ o = s.taboption('advanced', form.Value, 'serversfile',
_('Additional servers file'),
- _('This file may contain lines like \'server=/domain/1.2.3.4\' or \'server=1.2.3.4\' for domain-specific or full upstream <abbr title="Domain Name System">DNS</abbr> servers.'));
+ _('File listing upstream resolvers, optionally domain-specific, e.g. <code>server=1.2.3.4</code>, <code>server=/domain/1.2.3.4</code>.'));
+ o.placeholder = '/etc/dnsmasq.servers';
- s.taboption('advanced', form.Flag, 'strictorder',
+ o = s.taboption('advanced', form.Flag, 'strictorder',
_('Strict order'),
- _('<abbr title="Domain Name System">DNS</abbr> servers will be queried in the order of the resolvfile')).optional = true;
-
- s.taboption('advanced', form.Flag, 'allservers',
- _('All Servers'),
- _('Query all available upstream <abbr title="Domain Name System">DNS</abbr> servers')).optional = true;
-
- o = s.taboption('advanced', form.DynamicList, 'bogusnxdomain', _('Bogus NX Domain Override'),
- _('List of hosts that supply bogus NX domain results'));
-
+ _('Upstream resolvers will be queried in the order of the resolv file.'));
o.optional = true;
- o.placeholder = '67.215.65.132';
-
-
- s.taboption('general', form.Flag, 'logqueries',
- _('Log queries'),
- _('Write received DNS requests to syslog')).optional = true;
-
- o = s.taboption('general', form.DynamicList, 'server', _('DNS forwardings'),
- _('List of <abbr title="Domain Name System">DNS</abbr> servers to forward requests to'));
+ o = s.taboption('advanced', form.Flag, 'allservers',
+ _('All servers'),
+ _('Query all available upstream resolvers.'));
o.optional = true;
- o.placeholder = '/example.org/10.1.2.3';
- o.validate = validateServerSpec;
-
-
- o = s.taboption('general', form.Flag, 'rebind_protection',
- _('Rebind protection'),
- _('Discard upstream RFC1918 responses'));
-
- o.rmempty = false;
-
- o = s.taboption('general', form.Flag, 'rebind_localhost',
- _('Allow localhost'),
- _('Allow upstream responses in the 127.0.0.0/8 range, e.g. for RBL services'));
-
- o.depends('rebind_protection', '1');
-
-
- o = s.taboption('general', form.DynamicList, 'rebind_domain',
- _('Domain whitelist'),
- _('List of domains to allow RFC1918 responses for'));
+ o = s.taboption('advanced', form.DynamicList, 'bogusnxdomain',
+ _('IPs to override with NXDOMAIN'),
+ _('List of IP addresses to convert into NXDOMAIN responses.'));
o.optional = true;
-
- o.depends('rebind_protection', '1');
- o.placeholder = 'ihost.netflix.com';
- o.validate = validateAddressList;
-
+ o.placeholder = '64.94.110.11';
o = s.taboption('advanced', form.Value, 'port',
- _('<abbr title="Domain Name System">DNS</abbr> server port'),
- _('Listening port for inbound DNS queries'));
-
+ _('DNS server port'),
+ _('Listening port for inbound DNS queries.'));
o.optional = true;
o.datatype = 'port';
o.placeholder = 53;
-
o = s.taboption('advanced', form.Value, 'queryport',
- _('<abbr title="Domain Name System">DNS</abbr> query port'),
- _('Fixed source port for outbound DNS queries'));
-
+ _('DNS query port'),
+ _('Fixed source port for outbound DNS queries.'));
o.optional = true;
o.datatype = 'port';
o.placeholder = _('any');
-
o = s.taboption('advanced', form.Value, 'dhcpleasemax',
- _('<abbr title="maximal">Max.</abbr> <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> leases'),
- _('Maximum allowed number of active DHCP leases'));
-
+ _('Max. DHCP leases'),
+ _('Maximum allowed number of active DHCP leases.'));
o.optional = true;
o.datatype = 'uinteger';
o.placeholder = _('unlimited');
-
o = s.taboption('advanced', form.Value, 'ednspacket_max',
- _('<abbr title="maximal">Max.</abbr> <abbr title="Extension Mechanisms for Domain Name System">EDNS0</abbr> packet size'),
- _('Maximum allowed size of EDNS.0 UDP packets'));
-
+ _('Max. EDNS0 packet size'),
+ _('Maximum allowed size of EDNS0 UDP packets.'));
o.optional = true;
o.datatype = 'uinteger';
o.placeholder = 1280;
-
o = s.taboption('advanced', form.Value, 'dnsforwardmax',
- _('<abbr title="maximal">Max.</abbr> concurrent queries'),
- _('Maximum allowed number of concurrent DNS queries'));
-
+ _('Max. concurrent queries'),
+ _('Maximum allowed number of concurrent DNS queries.'));
o.optional = true;
o.datatype = 'uinteger';
o.placeholder = 150;
o = s.taboption('advanced', form.Value, 'cachesize',
_('Size of DNS query cache'),
- _('Number of cached DNS entries (max is 10000, 0 is no caching)'));
+ _('Number of cached DNS entries, 10000 is maximum, 0 is no caching.'));
o.optional = true;
o.datatype = 'range(0,10000)';
- o.placeholder = 150;
+ o.placeholder = 1000;
- s.taboption('tftp', form.Flag, 'enable_tftp',
- _('Enable TFTP server')).optional = true;
+ o = s.taboption('pxe_tftp', form.Flag, 'enable_tftp',
+ _('Enable TFTP server'),
+ _('Enable the built-in single-instance TFTP server.'));
+ o.optional = true;
- o = s.taboption('tftp', form.Value, 'tftp_root',
+ o = s.taboption('pxe_tftp', form.Value, 'tftp_root',
_('TFTP server root'),
- _('Root directory for files served via TFTP'));
-
- o.optional = true;
+ _('Root directory for files served via TFTP. <em>Enable TFTP server</em> and <em>TFTP server root</em> turn on the TFTP server and serve files from <em>TFTP server root</em>.'));
o.depends('enable_tftp', '1');
+ o.optional = true;
o.placeholder = '/';
-
- o = s.taboption('tftp', form.Value, 'dhcp_boot',
+ o = s.taboption('pxe_tftp', form.Value, 'dhcp_boot',
_('Network boot image'),
- _('Filename of the boot image advertised to clients'));
-
- o.optional = true;
+ _('Filename of the boot image advertised to clients.'));
o.depends('enable_tftp', '1');
+ o.optional = true;
o.placeholder = 'pxelinux.0';
- o = s.taboption('general', form.Flag, 'localservice',
- _('Local Service Only'),
- _('Limit DNS service to subnets interfaces on which we are serving DNS.'));
- o.optional = false;
- o.rmempty = false;
+ /* PXE - https://openwrt.org/docs/guide-user/base-system/dhcp#booting_options */
+ o = s.taboption('pxe_tftp', form.SectionValue, '__pxe__', form.GridSection, 'boot', null,
+ _('Special <abbr title="Preboot eXecution Environment">PXE</abbr> boot options for Dnsmasq.'));
+ ss = o.subsection;
+ ss.addremove = true;
+ ss.anonymous = true;
+ ss.nodescriptions = true;
+
+ so = ss.option(form.Value, 'filename',
+ _('Filename'),
+ _('Host requests this filename from the boot server.'));
+ so.optional = false;
+ so.placeholder = 'pxelinux.0';
+
+ so = ss.option(form.Value, 'servername',
+ _('Server name'),
+ _('The hostname of the boot server'));
+ so.optional = false;
+ so.placeholder = 'myNAS';
+
+ so = ss.option(form.Value, 'serveraddress',
+ _('Server address'),
+ _('The IP address of the boot server'));
+ so.optional = false;
+ so.placeholder = '192.168.1.2';
+
+ so = ss.option(form.DynamicList, 'dhcp_option',
+ _('DHCP Options'),
+ _('Options for the Network-ID. (Note: needs also Network-ID.) E.g. "<code>42,192.168.1.4</code>" for NTP server, "<code>3,192.168.4.4</code>" for default route. <code>0.0.0.0</code> means "the address of the system running dnsmasq".'));
+ so.optional = true;
+ so.placeholder = '42,192.168.1.4';
+
+ so = ss.option(widgets.DeviceSelect, 'networkid',
+ _('Network-ID'),
+ _('Apply DHCP Options to this net. (Empty = all clients).'));
+ so.optional = true;
+ so.noaliases = true;
+
+ so = ss.option(form.Flag, 'force',
+ _('Force'),
+ _('Always send DHCP Options. Sometimes needed, with e.g. PXELinux.'));
+ so.optional = true;
+
+ so = ss.option(form.Value, 'instance',
+ _('Instance'),
+ _('Dnsmasq instance to which this boot section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
+ so.optional = true;
+
+ Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
+ so.value(index, '%s (Domain: %s, Local: %s)'.format(index, val.domain || '?', val.local || '?'));
+ });
- o = s.taboption('general', form.Flag, 'nonwildcard',
- _('Non-wildcard'),
- _('Bind dynamically to interfaces rather than wildcard address (recommended as linux default)'));
- o.default = o.enabled;
- o.optional = false;
- o.rmempty = true;
+ o = s.taboption('srvhosts', form.SectionValue, '__srvhosts__', form.TableSection, 'srvhost', null,
+ _('Bind service records to a domain name: specify the location of services. See <a href="%s">RFC2782</a>.').format('https://datatracker.ietf.org/doc/html/rfc2782')
+ + '<br />' + _('_service: _sip, _ldap, _imap, _stun, _xmpp-client, … . (Note: while _http is possible, no browsers support SRV records.)')
+ + '<br />' + _('_proto: _tcp, _udp, _sctp, _quic, … .')
+ + '<br />' + _('You may add multiple records for the same Target.')
+ + '<br />' + _('Larger weights (of the same prio) are given a proportionately higher probability of being selected.'));
- o = s.taboption('general', form.DynamicList, 'interface',
- _('Listen Interfaces'),
- _('Limit listening to these interfaces, and loopback.'));
- o.optional = true;
+ ss = o.subsection;
- o = s.taboption('general', form.DynamicList, 'notinterface',
- _('Exclude interfaces'),
- _('Prevent listening on these interfaces.'));
- o.optional = true;
+ ss.addremove = true;
+ ss.anonymous = true;
+ ss.sortable = true;
+ ss.rowcolors = true;
- o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
- _('Static leases are used to assign fixed IP addresses and symbolic hostnames to DHCP clients. They are also required for non-dynamic interface configurations where only hosts with a corresponding lease are served.') + '<br />' +
- _('Use the <em>Add</em> Button to add a new lease entry. The <em>MAC-Address</em> identifies the host, the <em>IPv4-Address</em> specifies the fixed address to use, and the <em>Hostname</em> is assigned as a symbolic name to the requesting host. The optional <em>Lease time</em> can be used to set non-standard host-specific lease time, e.g. 12h, 3d or infinite.'));
+ so = ss.option(form.Value, 'srv', _('SRV'), _('Syntax: <code>_service._proto.example.com</code>.'));
+ so.rmempty = false;
+ so.datatype = 'hostname';
+ so.placeholder = '_sip._tcp.example.com';
+
+ so = ss.option(form.Value, 'target', _('Target'), _('CNAME or fqdn'));
+ so.rmempty = false;
+ so.datatype = 'hostname';
+ so.placeholder = 'sip.example.com';
+
+ so = ss.option(form.Value, 'port', _('Port'));
+ so.rmempty = false;
+ so.datatype = 'port';
+ so.placeholder = '5060';
+
+ so = ss.option(form.Value, 'class', _('Priority'), _('Ordinal: lower comes first.'));
+ so.rmempty = true;
+ so.datatype = 'range(0,65535)';
+ so.placeholder = '10';
+
+ so = ss.option(form.Value, 'weight', _('Weight'));
+ so.rmempty = true;
+ so.datatype = 'range(0,65535)';
+ so.placeholder = '50';
+
+ o = s.taboption('mxhosts', form.SectionValue, '__mxhosts__', form.TableSection, 'mxhost', null,
+ _('Bind service records to a domain name: specify the location of services.')
+ + '<br />' + _('You may add multiple records for the same domain.'));
ss = o.subsection;
ss.addremove = true;
ss.anonymous = true;
+ ss.sortable = true;
+ ss.rowcolors = true;
+ ss.nodescriptions = true;
+
+ so = ss.option(form.Value, 'domain', _('Domain'));
+ so.rmempty = false;
+ so.datatype = 'hostname';
+ so.placeholder = 'example.com';
+
+ so = ss.option(form.Value, 'relay', _('Relay'));
+ so.rmempty = false;
+ so.datatype = 'hostname';
+ so.placeholder = 'relay.example.com';
+
+ so = ss.option(form.Value, 'pref', _('Priority'), _('Ordinal: lower comes first.'));
+ so.rmempty = true;
+ so.datatype = 'range(0,65535)';
+ so.placeholder = '0';
+
+ o = s.taboption('cnamehosts', form.SectionValue, '__cname__', form.TableSection, 'cname', null,
+ _('Set an alias for a hostname.'));
+
+ ss = o.subsection;
+
+ ss.addremove = true;
+ ss.anonymous = true;
+ ss.sortable = true;
+ ss.rowcolors = true;
+ ss.nodescriptions = true;
+
+ so = ss.option(form.Value, 'cname', _('Domain'));
+ so.rmempty = false;
+ so.datatype = 'hostname';
+ so.placeholder = 'www.example.com';
+
+ so = ss.option(form.Value, 'target', _('Target'));
+ so.rmempty = false;
+ so.datatype = 'hostname';
+ so.placeholder = 'example.com';
+
+ o = s.taboption('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
+ _('Hostnames are used to bind a domain name to an IP address. This setting is redundant for hostnames already configured with static leases, but it can be useful to rebind an FQDN.'));
+
+ ss = o.subsection;
+
+ ss.addremove = true;
+ ss.anonymous = true;
+ ss.sortable = true;
so = ss.option(form.Value, 'name', _('Hostname'));
+ so.rmempty = false;
+ so.datatype = 'hostname';
+
+ so = ss.option(form.Value, 'ip', _('IP address'));
+ so.rmempty = false;
+ so.datatype = 'ipaddr';
+
+ var ipaddrs = {};
+
+ Object.keys(hosts).forEach(function(mac) {
+ var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4);
+
+ for (var i = 0; i < addrs.length; i++)
+ ipaddrs[addrs[i]] = hosts[mac].name || mac;
+ });
+
+ L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
+ so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
+ });
+
+ o = s.taboption('ipsets', form.SectionValue, '__ipsets__', form.GridSection, 'ipset', null,
+ _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.'));
+
+ ss = o.subsection;
+
+ ss.addremove = true;
+ ss.anonymous = true;
+ ss.sortable = true;
+
+ so = ss.option(form.DynamicList, 'name', _('IP set'));
+ so.rmempty = false;
+ so.datatype = 'string';
+
+ so = ss.option(form.DynamicList, 'domain', _('Domain'));
+ so.rmempty = false;
+ so.datatype = 'hostname';
+
+ o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
+ _('Static leases are used to assign fixed IP addresses and symbolic hostnames to DHCP clients. They are also required for non-dynamic interface configurations where only hosts with a corresponding lease are served.') + '<br /><br />' +
+ _('Use the <em>Add</em> Button to add a new lease entry. The <em>MAC address</em> identifies the host, the <em>IPv4 address</em> specifies the fixed address to use, and the <em>Hostname</em> is assigned as a symbolic name to the requesting host. The optional <em>Lease time</em> can be used to set non-standard host-specific lease time, e.g. 12h, 3d or infinite.') + '<br /><br />' +
+ _('The tag construct filters which host directives are used; more than one tag can be provided, in this case the request must match all of them. Tagged directives are used in preference to untagged ones. Note that one of mac, duid or hostname still needs to be specified (can be a wildcard).'));
+
+ ss = o.subsection;
+
+ ss.addremove = true;
+ ss.anonymous = true;
+ ss.sortable = true;
+ ss.nodescriptions = true;
+ ss.max_cols = 8;
+ ss.modaltitle = _('Edit static lease');
+
+ so = ss.option(form.Value, 'name',
+ _('Hostname'),
+ _('Optional hostname to assign'));
so.validate = validateHostname;
so.rmempty = true;
so.write = function(section, value) {
@@ -421,23 +795,35 @@ return view.extend({
uci.unset('dhcp', section, 'dns');
};
- so = ss.option(form.Value, 'mac', _('<abbr title="Media Access Control">MAC</abbr>-Address'));
- so.datatype = 'list(unique(macaddr))';
+ so = ss.option(form.Value, 'mac',
+ _('MAC address(es)'),
+ _('The hardware address(es) of this entry/host, separated by spaces.') + '<br /><br />' +
+ _('In DHCPv4, it is possible to include more than one mac address. This allows an IP address to be associated with multiple macaddrs, and dnsmasq abandons a DHCP lease to one of the macaddrs when another asks for a lease. It only works reliably if only one of the macaddrs is active at any time.'));
+ //As a special case, in DHCPv4, it is possible to include more than one hardware address. eg: --dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.2 This allows an IP address to be associated with multiple hardware addresses, and gives dnsmasq permission to abandon a DHCP lease to one of the hardware addresses when another one asks for a lease
+ so.validate = function(section_id, value) {
+ var macaddrs = L.toArray(value);
+
+ for (var i = 0; i < macaddrs.length; i++)
+ if (!macaddrs[i].match(/^([a-fA-F0-9]{2}|\*):([a-fA-F0-9]{2}:|\*:){4}(?:[a-fA-F0-9]{2}|\*)$/))
+ return _('Expecting a valid MAC address, optionally including wildcards');
+
+ return true;
+ };
so.rmempty = true;
so.cfgvalue = function(section) {
- var macs = uci.get('dhcp', section, 'mac'),
+ var macs = L.toArray(uci.get('dhcp', section, 'mac')),
result = [];
- if (!Array.isArray(macs))
- macs = (macs != null && macs != '') ? macs.split(/\ss+/) : [];
-
for (var i = 0, mac; (mac = macs[i]) != null; i++)
- if (/^([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2})$/.test(mac))
- result.push('%02X:%02X:%02X:%02X:%02X:%02X'.format(
+ if (/^([0-9a-fA-F]{1,2}|\*):([0-9a-fA-F]{1,2}|\*):([0-9a-fA-F]{1,2}|\*):([0-9a-fA-F]{1,2}|\*):([0-9a-fA-F]{1,2}|\*):([0-9a-fA-F]{1,2}|\*)$/.test(mac)) {
+ var m = [
parseInt(RegExp.$1, 16), parseInt(RegExp.$2, 16),
parseInt(RegExp.$3, 16), parseInt(RegExp.$4, 16),
- parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16)));
+ parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16)
+ ];
+ result.push(m.map(function(n) { return isNaN(n) ? '*' : '%02X'.format(n) }).join(':'));
+ }
return result.length ? result.join(' ') : null;
};
so.renderWidget = function(section_id, option_index, cfgvalue) {
@@ -446,7 +832,11 @@ return view.extend({
node.addEventListener('cbi-dropdown-change', L.bind(function(ipopt, section_id, ev) {
var mac = ev.detail.value.value;
- if (mac == null || mac == '' || !hosts[mac] || !hosts[mac].ipv4)
+ if (mac == null || mac == '' || !hosts[mac])
+ return;
+
+ var iphint = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
+ if (iphint == null)
return;
var ip = ipopt.formvalue(section_id);
@@ -455,46 +845,104 @@ return view.extend({
var node = ipopt.map.findElement('id', ipopt.cbid(section_id));
if (node)
- dom.callClassMethod(node, 'setValue', hosts[mac].ipv4);
+ dom.callClassMethod(node, 'setValue', iphint);
}, this, ipopt, section_id));
return node;
};
+ so.validate = validateMACAddr.bind(so, pools);
Object.keys(hosts).forEach(function(mac) {
- var hint = hosts[mac].name || hosts[mac].ipv4;
+ var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
});
- so = ss.option(form.Value, 'ip', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Address'));
+ so = ss.option(form.Value, 'ip', _('IPv4 address'), _('The IP address to be used for this host, or <em>ignore</em> to ignore any DHCP request from this host.'));
+ so.value('ignore', _('Ignore'));
so.datatype = 'or(ip4addr,"ignore")';
so.validate = function(section, value) {
- var mac = this.map.lookupOption('mac', section),
- name = this.map.lookupOption('name', section),
- m = mac ? mac[0].formvalue(section) : null,
- n = name ? name[0].formvalue(section) : null;
+ var m = this.section.formvalue(section, 'mac'),
+ n = this.section.formvalue(section, 'name');
if ((m == null || m == '') && (n == null || n == ''))
- return _('One of hostname or mac address must be specified!');
+ return _('One of hostname or MAC address must be specified!');
- return true;
- };
- Object.keys(hosts).forEach(function(mac) {
- if (hosts[mac].ipv4) {
- var hint = hosts[mac].name;
- so.value(hosts[mac].ipv4, hint ? '%s (%s)'.format(hosts[mac].ipv4, hint) : hosts[mac].ipv4);
+ if (value == null || value == '' || value == 'ignore')
+ return true;
+
+ var leases = uci.sections('dhcp', 'host');
+
+ for (var i = 0; i < leases.length; i++)
+ if (leases[i]['.name'] != section && leases[i].ip == value)
+ return _('The IP address %h is already used by another static lease').format(value);
+
+ for (var i = 0; i < pools.length; i++) {
+ var net_mask = calculateNetwork(value, pools[i].netmask);
+
+ if (net_mask && net_mask[0] == pools[i].network)
+ return true;
}
+
+ return _('The IP address is outside of any DHCP pool address range');
+ };
+
+ L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
+ so.value(ipv4, ipaddrs[ipv4] ? '%s (%s)'.format(ipv4, ipaddrs[ipv4]) : ipv4);
});
- so = ss.option(form.Value, 'leasetime', _('Lease time'));
+ so = ss.option(form.Value, 'leasetime',
+ _('Lease time'),
+ _('Host-specific lease time, e.g. <code>5m</code>, <code>3h</code>, <code>7d</code>.'));
so.rmempty = true;
-
- so = ss.option(form.Value, 'duid', _('<abbr title="The DHCP Unique Identifier">DUID</abbr>'));
+ so.value('5m', _('5m (5 minutes)'));
+ so.value('3h', _('3h (3 hours)'));
+ so.value('12h', _('12h (12 hours - default)'));
+ so.value('7d', _('7d (7 days)'));
+ so.value('infinite', _('infinite (lease does not expire)'));
+
+ so = ss.option(form.Value, 'duid',
+ _('DUID'),
+ _('The DHCPv6-DUID (DHCP unique identifier) of this host.'));
so.datatype = 'and(rangelength(20,36),hexstring)';
Object.keys(duids).forEach(function(duid) {
so.value(duid, '%s (%s)'.format(duid, duids[duid].hostname || duids[duid].macaddr || duids[duid].ip6addr || '?'));
});
- so = ss.option(form.Value, 'hostid', _('<abbr title="Internet Protocol Version 6">IPv6</abbr>-Suffix (hex)'));
+ so = ss.option(form.Value, 'hostid',
+ _('IPv6-Suffix (hex)'),
+ _('The IPv6 interface identifier (address suffix) as hexadecimal number (max. 16 chars).'));
+ so.datatype = 'and(rangelength(0,16),hexstring)';
+
+ so = ss.option(form.DynamicList, 'tag',
+ _('Tag'),
+ _('Assign new, freeform tags to this entry.'));
+
+ so = ss.option(form.DynamicList, 'match_tag',
+ _('Match Tag'),
+ _('When a host matches an entry then the special tag <em>known</em> is set. Use <em>known</em> to match all known hosts.') + '<br /><br />' +
+ _('Ignore requests from unknown machines using <em>!known</em>.') + '<br /><br />' +
+ _('If a host matches an entry which cannot be used because it specifies an address on a different subnet, the tag <em>known-othernet</em> is set.'));
+ so.value('known', _('known'));
+ so.value('!known', _('!known (not known)'));
+ so.value('known-othernet', _('known-othernet (on different subnet)'));
+ so.optional = true;
+
+ so = ss.option(form.Value, 'instance',
+ _('Instance'),
+ _('Dnsmasq instance to which this DHCP host section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
+ so.optional = true;
+
+ Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
+ so.value(index, '%s (Domain: %s, Local: %s)'.format(index, val.domain || '?', val.local || '?'));
+ });
+
+
+ so = ss.option(form.Flag, 'broadcast',
+ _('Broadcast'),
+ _('Force broadcast DHCP response.'));
+
+ so = ss.option(form.Flag, 'dns',
+ _('Forward/reverse DNS'),
+ _('Add static forward and reverse DNS entries for this host.'));
o = s.taboption('leases', CBILeaseStatus, '__status__');
@@ -518,8 +966,17 @@ return view.extend({
else
exp = '%t'.format(lease.expires);
+ var hint = lease.macaddr ? hosts[lease.macaddr] : null,
+ name = hint ? hint.name : null,
+ host = null;
+
+ if (name && lease.hostname && lease.hostname != name)
+ host = '%s (%s)'.format(lease.hostname, name);
+ else if (lease.hostname)
+ host = lease.hostname;
+
return [
- lease.hostname || '?',
+ host || '-',
lease.ipaddr,
lease.macaddr,
exp
@@ -540,7 +997,7 @@ return view.extend({
exp = '%t'.format(lease.expires);
var hint = lease.macaddr ? hosts[lease.macaddr] : null,
- name = hint ? (hint.name || hint.ipv4 || hint.ipv6) : null,
+ name = hint ? (hint.name || L.toArray(hint.ipaddrs || hint.ipv4)[0] || L.toArray(hint.ip6addrs || hint.ipv6)[0]) : null,
host = null;
if (name && lease.hostname && lease.hostname != name && lease.ip6addr != name)
diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/diagnostics.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/diagnostics.js
index 1855ee6422..1bfa95501a 100644
--- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/diagnostics.js
+++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/diagnostics.js
@@ -4,6 +4,7 @@
'require fs';
'require ui';
'require uci';
+'require network';
return view.extend({
handleCommand: function(exec, args) {
@@ -13,8 +14,7 @@ return view.extend({
buttons[i].setAttribute('disabled', 'true');
return fs.exec(exec, args).then(function(res) {
- var out = document.querySelector('.command-output');
- out.style.display = '';
+ var out = document.querySelector('textarea');
dom.content(out, [ res.stdout || '', res.stderr || '' ]);
}).catch(function(err) {
@@ -36,7 +36,7 @@ return view.extend({
handleTraceroute: function(ev, cmd) {
var exec = cmd || 'traceroute',
addr = ev.currentTarget.parentNode.previousSibling.value,
- args = (exec == 'traceroute') ? [ '-q', '1', '-w', '1', '-n', addr ] : [ '-q', '1', '-w', '2', '-n', addr ];
+ args = (exec == 'traceroute') ? [ '-4', '-q', '1', '-w', '1', '-n', '-m', String(L.env.rpctimeout || 20), addr ] : [ '-q', '1', '-w', '2', '-n', addr ];
return this.handleCommand(exec, args);
},
@@ -47,12 +47,20 @@ return view.extend({
return this.handleCommand('nslookup', [ addr ]);
},
+ handleArpScan: function(ev, cmd) {
+ var addr = ev.currentTarget.parentNode.previousSibling.value;
+
+ return this.handleCommand('arp-scan', [ '-l', '-I', addr ]);
+ },
+
load: function() {
return Promise.all([
L.resolveDefault(fs.stat('/bin/ping6'), {}),
L.resolveDefault(fs.stat('/usr/bin/ping6'), {}),
L.resolveDefault(fs.stat('/bin/traceroute6'), {}),
L.resolveDefault(fs.stat('/usr/bin/traceroute6'), {}),
+ L.resolveDefault(fs.stat('/usr/bin/arp-scan'), {}),
+ network.getDevices(),
uci.load('luci')
]);
},
@@ -60,15 +68,15 @@ return view.extend({
render: function(res) {
var has_ping6 = res[0].path || res[1].path,
has_traceroute6 = res[2].path || res[3].path,
+ has_arpscan = res[4].path,
+ devices = res[5],
dns_host = uci.get('luci', 'diag', 'dns') || 'openwrt.org',
ping_host = uci.get('luci', 'diag', 'ping') || 'openwrt.org',
route_host = uci.get('luci', 'diag', 'route') || 'openwrt.org';
- return E([], [
- E('h2', {}, [ _('Network Utilities') ]),
- E('div', { 'class': 'table' }, [
- E('div', { 'class': 'tr' }, [
- E('div', { 'class': 'td left' }, [
+ var table = E('table', { 'class': 'table' }, [
+ E('tr', { 'class': 'tr' }, [
+ E('td', { 'class': 'td left', 'style': 'overflow:initial' }, [
E('input', {
'style': 'margin:5px 0',
'type': 'text',
@@ -91,7 +99,7 @@ return view.extend({
])
]),
- E('div', { 'class': 'td left' }, [
+ E('td', { 'class': 'td left', 'style': 'overflow:initial' }, [
E('input', {
'style': 'margin:5px 0',
'type': 'text',
@@ -114,7 +122,7 @@ return view.extend({
])
]),
- E('div', { 'class': 'td left' }, [
+ E('td', { 'class': 'td left' }, [
E('input', {
'style': 'margin:5px 0',
'type': 'text',
@@ -126,11 +134,45 @@ return view.extend({
'click': ui.createHandlerFn(this, 'handleNslookup')
}, [ _('Nslookup') ])
])
- ])
+ ]),
+
+ has_arpscan ? E('td', { 'class': 'td left' }, [
+ E('select', {
+ 'style': 'margin:5px 0'
+ }, devices.map(function(device) {
+ if (!device.isUp())
+ return E([]);
+
+ return E('option', { 'value': device.getName() }, [ device.getI18n() ]);
+ })),
+ E('span', { 'class': 'diag-action' }, [
+ E('button', {
+ 'class': 'cbi-button cbi-button-action',
+ 'click': ui.createHandlerFn(this, 'handleArpScan')
+ }, [ _('Arp-scan') ])
+ ])
+ ]) : E([]),
])
- ]),
- E('pre', { 'class': 'command-output', 'style': 'display:none' })
+ ]);
+
+ var view = E('div', { 'class': 'cbi-map'}, [
+ E('h2', {}, [ _('Diagnostics') ]),
+ E('div', { 'class': 'cbi-map-descr'}, _('Execution of various network commands to check the connection and name resolution to other systems.')),
+ table,
+ E('div', {'class': 'cbi-section'}, [
+ E('div', { 'id' : 'command-output'},
+ E('textarea', {
+ 'id': 'widget.command-output',
+ 'style': 'width: 100%; font-family:monospace; white-space:pre',
+ 'readonly': true,
+ 'wrap': 'off',
+ 'rows': '20'
+ })
+ )
+ ])
]);
+
+ return view;
},
handleSaveApply: null,
diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/hosts.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/hosts.js
deleted file mode 100644
index cd0dacbf67..0000000000
--- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/hosts.js
+++ /dev/null
@@ -1,43 +0,0 @@
-'use strict';
-'require view';
-'require rpc';
-'require form';
-
-return view.extend({
- callHostHints: rpc.declare({
- object: 'luci-rpc',
- method: 'getHostHints',
- expect: { '': {} }
- }),
-
- load: function() {
- return this.callHostHints();
- },
-
- render: function(hosts) {
- var m, s, o;
-
- m = new form.Map('dhcp', _('Hostnames'));
-
- s = m.section(form.GridSection, 'domain', _('Host entries'));
- s.addremove = true;
- s.anonymous = true;
- s.sortable = true;
-
- o = s.option(form.Value, 'name', _('Hostname'));
- o.datatype = 'hostname';
- o.rmempty = true;
-
- o = s.option(form.Value, 'ip', _('IP address'));
- o.datatype = 'ipaddr';
- o.rmempty = true;
- L.sortedKeys(hosts, 'ipv4', 'addr').forEach(function(mac) {
- o.value(hosts[mac].ipv4, '%s (%s)'.format(
- hosts[mac].ipv4,
- hosts[mac].name || mac
- ));
- });
-
- return m.render();
- }
-});
diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js
index bee5753055..3748f04b05 100644
--- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js
+++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js
@@ -9,6 +9,7 @@
'require network';
'require firewall';
'require tools.widgets as widgets';
+'require tools.network as nettools';
var isReadonlyView = !L.hasViewPermission() || null;
@@ -127,7 +128,7 @@ function render_modal_status(node, ifc) {
function render_ifacebox_status(node, ifc) {
var dev = ifc.getL3Device() || ifc.getDevice(),
- subdevs = ifc.getDevices(),
+ subdevs = dev ? dev.getPorts() : null,
c = [ render_iface(dev, ifc.isAlias()) ];
if (subdevs && subdevs.length) {
@@ -212,23 +213,71 @@ function iface_updown(up, id, ev, force) {
function get_netmask(s, use_cfgvalue) {
var readfn = use_cfgvalue ? 'cfgvalue' : 'formvalue',
- addropt = s.children.filter(function(o) { return o.option == 'ipaddr'})[0],
- addrvals = addropt ? L.toArray(addropt[readfn](s.section)) : [],
- maskopt = s.children.filter(function(o) { return o.option == 'netmask'})[0],
- maskval = maskopt ? maskopt[readfn](s.section) : null,
- firstsubnet = maskval ? addrvals[0] + '/' + maskval : addrvals.filter(function(a) { return a.indexOf('/') > 0 })[0];
+ addrs = L.toArray(s[readfn](s.section, 'ipaddr')),
+ mask = s[readfn](s.section, 'netmask'),
+ firstsubnet = mask ? addrs[0] + '/' + mask : addrs.filter(function(a) { return a.indexOf('/') > 0 })[0];
if (firstsubnet == null)
return null;
- var mask = firstsubnet.split('/')[1];
+ var subnetmask = firstsubnet.split('/')[1];
- if (!isNaN(mask))
- mask = network.prefixToMask(+mask);
+ if (!isNaN(subnetmask))
+ subnetmask = network.prefixToMask(+subnetmask);
- return mask;
+ return subnetmask;
}
+function has_peerdns(proto) {
+ switch (proto) {
+ case 'dhcp':
+ case 'dhcpv6':
+ case 'qmi':
+ case 'ppp':
+ case 'pppoe':
+ case 'pppoa':
+ case 'pptp':
+ case 'openvpn':
+ case 'sstp':
+ return true;
+ }
+
+ return false;
+}
+
+var cbiRichListValue = form.ListValue.extend({
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var choices = this.transformChoices();
+ var widget = new ui.Dropdown((cfgvalue != null) ? cfgvalue : this.default, choices, {
+ id: this.cbid(section_id),
+ sort: this.keylist,
+ optional: true,
+ select_placeholder: this.select_placeholder || this.placeholder,
+ custom_placeholder: this.custom_placeholder || this.placeholder,
+ validate: L.bind(this.validate, this, section_id),
+ disabled: (this.readonly != null) ? this.readonly : this.map.readonly
+ });
+
+ return widget.render();
+ },
+
+ value: function(value, title, description) {
+ if (description) {
+ form.ListValue.prototype.value.call(this, value, E([], [
+ E('span', { 'class': 'hide-open' }, [ title ]),
+ E('div', { 'class': 'hide-close', 'style': 'min-width:25vw' }, [
+ E('strong', [ title ]),
+ E('br'),
+ E('span', { 'style': 'white-space:normal' }, description)
+ ])
+ ]));
+ }
+ else {
+ form.ListValue.prototype.value.call(this, value, title);
+ }
+ }
+});
+
return view.extend({
poll_status: function(map, networks) {
var resolveZone = null;
@@ -287,20 +336,142 @@ return view.extend({
btn2.disabled = isReadonlyView || btn1.classList.contains('spinning') || btn2.classList.contains('spinning') || dynamic || disabled;
}
+ document.querySelectorAll('.port-status-device[data-device]').forEach(function(node) {
+ nettools.updateDevBadge(node, network.instantiateDevice(node.getAttribute('data-device')));
+ });
+
+ document.querySelectorAll('.port-status-link[data-device]').forEach(function(node) {
+ nettools.updatePortStatus(node, network.instantiateDevice(node.getAttribute('data-device')));
+ });
+
return Promise.all([ resolveZone, network.flushCache() ]);
},
load: function() {
return Promise.all([
network.getDSLModemType(),
+ network.getDevices(),
+ fs.lines('/etc/iproute2/rt_tables'),
+ L.resolveDefault(fs.read('/usr/lib/opkg/info/netifd.control')),
uci.changes()
]);
},
+ interfaceBridgeWithIfnameSections: function() {
+ return uci.sections('network', 'interface').filter(function(ns) {
+ return ns.type == 'bridge' && !ns.ports && ns.ifname;
+ });
+ },
+
+ deviceWithIfnameSections: function() {
+ return uci.sections('network', 'device').filter(function(ns) {
+ return ns.type == 'bridge' && !ns.ports && ns.ifname;
+ });
+ },
+
+ interfaceWithIfnameSections: function() {
+ return uci.sections('network', 'interface').filter(function(ns) {
+ return !ns.device && ns.ifname;
+ });
+ },
+
+ handleBridgeMigration: function(ev) {
+ var tasks = [];
+
+ this.interfaceBridgeWithIfnameSections().forEach(function(ns) {
+ var device_name = 'br-' + ns['.name'];
+
+ tasks.push(uci.callAdd('network', 'device', null, {
+ 'name': device_name,
+ 'type': 'bridge',
+ 'ports': L.toArray(ns.ifname),
+ 'mtu': ns.mtu,
+ 'macaddr': ns.macaddr,
+ 'igmp_snooping': ns.igmp_snooping
+ }));
+
+ tasks.push(uci.callSet('network', ns['.name'], {
+ 'type': '',
+ 'ifname': '',
+ 'mtu': '',
+ 'macaddr': '',
+ 'igmp_snooping': '',
+ 'device': device_name
+ }));
+ });
+
+ return Promise.all(tasks)
+ .then(L.bind(ui.changes.init, ui.changes))
+ .then(L.bind(ui.changes.apply, ui.changes));
+ },
+
+ renderBridgeMigration: function() {
+ ui.showModal(_('Network bridge configuration migration'), [
+ E('p', _('The existing network configuration needs to be changed for LuCI to function properly.')),
+ E('p', _('Upon pressing "Continue", bridges configuration will be updated and the network will be restarted to apply the updated configuration.')),
+ E('div', { 'class': 'right' },
+ E('button', {
+ 'class': 'btn cbi-button-action important',
+ 'click': ui.createHandlerFn(this, 'handleBridgeMigration')
+ }, _('Continue')))
+ ]);
+ },
+
+ handleIfnameMigration: function(ev) {
+ var tasks = [];
+
+ this.deviceWithIfnameSections().forEach(function(ds) {
+ tasks.push(uci.callSet('network', ds['.name'], {
+ 'ifname': '',
+ 'ports': L.toArray(ds.ifname)
+ }));
+ });
+
+ this.interfaceWithIfnameSections().forEach(function(ns) {
+ tasks.push(uci.callSet('network', ns['.name'], {
+ 'ifname': '',
+ 'device': ns.ifname
+ }));
+ });
+
+ return Promise.all(tasks)
+ .then(L.bind(ui.changes.init, ui.changes))
+ .then(L.bind(ui.changes.apply, ui.changes));
+ },
+
+ renderIfnameMigration: function() {
+ ui.showModal(_('Network ifname configuration migration'), [
+ E('p', _('The existing network configuration needs to be changed for LuCI to function properly.')),
+ E('p', _('Upon pressing "Continue", ifname options will get renamed and the network will be restarted to apply the updated configuration.')),
+ E('div', { 'class': 'right' },
+ E('button', {
+ 'class': 'btn cbi-button-action important',
+ 'click': ui.createHandlerFn(this, 'handleIfnameMigration')
+ }, _('Continue')))
+ ]);
+ },
+
render: function(data) {
+ var netifdVersion = (data[3] || '').match(/Version: ([^\n]+)/);
+
+ if (netifdVersion && netifdVersion[1] >= "2021-05-26") {
+ if (this.interfaceBridgeWithIfnameSections().length)
+ return this.renderBridgeMigration();
+ else if (this.deviceWithIfnameSections().length || this.interfaceWithIfnameSections().length)
+ return this.renderIfnameMigration();
+ }
+
var dslModemType = data[0],
+ netDevs = data[1],
m, s, o;
+ var rtTables = data[2].map(function(l) {
+ var m = l.trim().match(/^(\d+)\s+(\S+)$/);
+ return m ? [ +m[1], m[2] ] : null;
+ }).filter(function(e) {
+ return e && e[0] > 0;
+ });
+
m = new form.Map('network');
m.tabbed = true;
m.chain('dhcp');
@@ -323,6 +494,8 @@ return view.extend({
s.tab('general', _('General Settings'));
s.tab('advanced', _('Advanced Settings'));
s.tab('physical', _('Physical Settings'));
+ s.tab('brport', _('Bridge port specific options'));
+ s.tab('bridgevlan', _('Bridge VLAN filtering'));
s.tab('firewall', _('Firewall Settings'));
s.tab('dhcp', _('DHCP Server'));
@@ -332,7 +505,7 @@ return view.extend({
};
s.modaltitle = function(section_id) {
- return _('Interfaces') + ' » ' + section_id.toUpperCase();
+ return _('Interfaces') + ' » ' + section_id;
};
s.renderRowActions = function(section_id) {
@@ -370,7 +543,7 @@ return view.extend({
s.addModalOptions = function(s) {
var protoval = uci.get('network', s.section, 'proto'),
protoclass = protoval ? network.getProtocol(protoval) : null,
- o, ifname_single, ifname_multi, proto_select, proto_switch, type, stp, igmp, ss, so;
+ o, proto_select, proto_switch, type, stp, igmp, ss, so;
if (!protoval)
return;
@@ -379,7 +552,7 @@ return view.extend({
var protocols = network.getProtocols();
protocols.sort(function(a, b) {
- return a.getProtocol() > b.getProtocol();
+ return L.naturalCompare(a.getProtocol(), b.getProtocol());
});
o = s.taboption('general', form.DummyValue, '_ifacestat_modal', _('Status'));
@@ -394,6 +567,7 @@ return view.extend({
}, this);
o.write = function() {};
+
proto_select = s.taboption('general', form.ListValue, 'proto', _('Protocol'));
proto_select.modalonly = true;
@@ -409,84 +583,16 @@ return view.extend({
.then(L.bind(this.renderMoreOptionsModal, this, s.section));
}, this);
+ o = s.taboption('general', widgets.DeviceSelect, '_net_device', _('Device'));
+ o.ucioption = 'device';
+ o.nobridges = false;
+ o.optional = false;
+ o.network = ifc.getName();
+
o = s.taboption('general', form.Flag, 'auto', _('Bring up on boot'));
o.modalonly = true;
o.default = o.enabled;
- type = s.taboption('physical', form.Flag, 'type', _('Bridge interfaces'), _('Creates a bridge over specified interface(s)'));
- type.modalonly = true;
- type.disabled = '';
- type.enabled = 'bridge';
- type.write = type.remove = function(section_id, value) {
- var protocol = network.getProtocol(proto_select.formvalue(section_id)),
- ifnameopt = this.section.children.filter(function(o) { return o.option == (value ? 'ifname_multi' : 'ifname_single') })[0];
-
- if (!protocol.isVirtual() && !this.isActive(section_id))
- return;
-
- var old_ifnames = [],
- devs = ifc.getDevices() || L.toArray(ifc.getDevice());
-
- for (var i = 0; i < devs.length; i++)
- old_ifnames.push(devs[i].getName());
-
- var new_ifnames = L.toArray(ifnameopt.formvalue(section_id));
-
- if (!value)
- new_ifnames.length = Math.max(new_ifnames.length, 1);
-
- old_ifnames.sort();
- new_ifnames.sort();
-
- for (var i = 0; i < Math.max(old_ifnames.length, new_ifnames.length); i++) {
- if (old_ifnames[i] != new_ifnames[i]) {
- // backup_ifnames()
- for (var j = 0; j < old_ifnames.length; j++)
- ifc.deleteDevice(old_ifnames[j]);
-
- for (var j = 0; j < new_ifnames.length; j++)
- ifc.addDevice(new_ifnames[j]);
-
- break;
- }
- }
-
- if (value)
- uci.set('network', section_id, 'type', 'bridge');
- else
- uci.unset('network', section_id, 'type');
- };
-
- stp = s.taboption('physical', form.Flag, 'stp', _('Enable <abbr title="Spanning Tree Protocol">STP</abbr>'), _('Enables the Spanning Tree Protocol on this bridge'));
-
- igmp = s.taboption('physical', form.Flag, 'igmp_snooping', _('Enable <abbr title="Internet Group Management Protocol">IGMP</abbr> snooping'), _('Enables IGMP snooping on this bridge'));
-
- ifname_single = s.taboption('physical', widgets.DeviceSelect, 'ifname_single', _('Interface'));
- ifname_single.nobridges = ifc.isBridge();
- ifname_single.noaliases = false;
- ifname_single.optional = false;
- ifname_single.network = ifc.getName();
- ifname_single.write = ifname_single.remove = function() {};
-
- ifname_multi = s.taboption('physical', widgets.DeviceSelect, 'ifname_multi', _('Interface'));
- ifname_multi.nobridges = ifc.isBridge();
- ifname_multi.noaliases = true;
- ifname_multi.multiple = true;
- ifname_multi.optional = true;
- ifname_multi.network = ifc.getName();
- ifname_multi.display_size = 6;
- ifname_multi.write = ifname_multi.remove = function() {};
-
- ifname_single.cfgvalue = ifname_multi.cfgvalue = function(section_id) {
- var devs = ifc.getDevices() || L.toArray(ifc.getDevice()),
- ifnames = [];
-
- for (var i = 0; i < devs.length; i++)
- ifnames.push(devs[i].getName());
-
- return ifnames;
- };
-
if (L.hasSystemFeature('firewall')) {
o = s.taboption('firewall', widgets.ZoneSelect, '_zone', _('Create / Assign firewall-zone'), _('Choose the firewall zone you want to assign to this interface. Select <em>unspecified</em> to remove the interface from the associated zone or fill out the <em>custom</em> field to define a new zone and attach the interface to it.'));
o.network = ifc.getName();
@@ -530,19 +636,10 @@ return view.extend({
if (protocols[i].getProtocol() != uci.get('network', s.section, 'proto'))
proto_switch.depends('proto', protocols[i].getProtocol());
-
- if (!protocols[i].isVirtual()) {
- type.depends('proto', protocols[i].getProtocol());
- stp.depends({ type: 'bridge', proto: protocols[i].getProtocol() });
- igmp.depends({ type: 'bridge', proto: protocols[i].getProtocol() });
- ifname_single.depends({ type: '', proto: protocols[i].getProtocol() });
- ifname_multi.depends({ type: 'bridge', proto: protocols[i].getProtocol() });
- }
}
if (L.hasSystemFeature('dnsmasq') || L.hasSystemFeature('odhcpd')) {
o = s.taboption('dhcp', form.SectionValue, '_dhcp', form.TypedSection, 'dhcp');
- o.depends('proto', 'static');
ss = o.subsection;
ss.uciconfig = 'dhcp';
@@ -552,6 +649,7 @@ return view.extend({
ss.tab('general', _('General Setup'));
ss.tab('advanced', _('Advanced Settings'));
ss.tab('ipv6', _('IPv6 Settings'));
+ ss.tab('ipv6-ra', _('IPv6 RA Settings'));
ss.filter = function(section_id) {
return (uci.get('dhcp', section_id, 'interface') == ifc.getName());
@@ -562,123 +660,468 @@ return view.extend({
E('p', _('No DHCP Server configured for this interface') + ' &#160; '),
E('button', {
'class': 'cbi-button cbi-button-add',
- 'title': _('Setup DHCP Server'),
+ 'title': _('Set up DHCP Server'),
'click': ui.createHandlerFn(this, function(section_id, ev) {
this.map.save(function() {
uci.add('dhcp', 'dhcp', section_id);
uci.set('dhcp', section_id, 'interface', section_id);
- uci.set('dhcp', section_id, 'start', 100);
- uci.set('dhcp', section_id, 'limit', 150);
- uci.set('dhcp', section_id, 'leasetime', '12h');
+
+ if (protoval == 'static') {
+ uci.set('dhcp', section_id, 'start', 100);
+ uci.set('dhcp', section_id, 'limit', 150);
+ uci.set('dhcp', section_id, 'leasetime', '12h');
+ }
+ else {
+ uci.set('dhcp', section_id, 'ignore', 1);
+ }
});
}, ifc.getName())
- }, _('Setup DHCP Server'))
+ }, _('Set up DHCP Server'))
]);
};
ss.taboption('general', form.Flag, 'ignore', _('Ignore interface'), _('Disable <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> for this interface.'));
- so = ss.taboption('general', form.Value, 'start', _('Start'), _('Lowest leased address as offset from the network address.'));
+ if (protoval == 'static') {
+ so = ss.taboption('general', form.Value, 'start', _('Start'), _('Lowest leased address as offset from the network address.'));
+ so.optional = true;
+ so.datatype = 'or(uinteger,ip4addr("nomask"))';
+ so.default = '100';
+
+ so = ss.taboption('general', form.Value, 'limit', _('Limit'), _('Maximum number of leased addresses.'));
+ so.optional = true;
+ so.datatype = 'uinteger';
+ so.default = '150';
+
+ so = ss.taboption('general', form.Value, 'leasetime', _('Lease time'), _('Expiry time of leased addresses, minimum is 2 minutes (<code>2m</code>).'));
+ so.optional = true;
+ so.default = '12h';
+
+ so = ss.taboption('advanced', form.Flag, 'dynamicdhcp', _('Dynamic <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>'), _('Dynamically allocate DHCP addresses for clients. If disabled, only clients having static leases will be served.'));
+ so.default = so.enabled;
+
+ ss.taboption('advanced', form.Flag, 'force', _('Force'), _('Force DHCP on this network even if another server is detected.'));
+
+ // XXX: is this actually useful?
+ //ss.taboption('advanced', form.Value, 'name', _('Name'), _('Define a name for this network.'));
+
+ so = ss.taboption('advanced', form.Value, 'netmask', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Netmask'), _('Override the netmask sent to clients. Normally it is calculated from the subnet that is served.'));
+ so.optional = true;
+ so.datatype = 'ip4addr';
+
+ so.render = function(option_index, section_id, in_table) {
+ this.placeholder = get_netmask(s, true);
+ return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]);
+ };
+
+ so.validate = function(section_id, value) {
+ var uielem = this.getUIElement(section_id);
+ if (uielem)
+ uielem.setPlaceholder(get_netmask(s, false));
+ return form.Value.prototype.validate.apply(this, [ section_id, value ]);
+ };
+
+ ss.taboption('advanced', form.DynamicList, 'dhcp_option', _('DHCP-Options'), _('Define additional DHCP options, for example "<code>6,192.168.2.1,192.168.2.2</code>" which advertises different DNS servers to clients.'));
+ }
+
+
+ var has_other_master = uci.sections('dhcp', 'dhcp').filter(function(s) {
+ return (s.interface != ifc.getName() && s.master == '1');
+ })[0];
+
+ so = ss.taboption('ipv6', form.Flag , 'master', _('Designated master'));
+ so.readonly = has_other_master ? true : false;
+ so.description = has_other_master
+ ? _('Interface "%h" is already marked as designated master.').format(has_other_master.interface || has_other_master['.name'])
+ : _('Set this interface as master for RA and DHCPv6 relaying as well as NDP proxying.')
+ ;
+
+ so.validate = function(section_id, value) {
+ var hybrid_downstream_desc = _('Operate in <em>relay mode</em> if a designated master interface is configured and active, otherwise fall back to <em>server mode</em>.'),
+ ndp_downstream_desc = _('Operate in <em>relay mode</em> if a designated master interface is configured and active, otherwise disable <abbr title="Neighbour Discovery Protocol">NDP</abbr> proxying.'),
+ hybrid_master_desc = _('Operate in <em>relay mode</em> if an upstream IPv6 prefix is present, otherwise disable service.'),
+ ra_server_allowed = true,
+ checked = this.formvalue(section_id),
+ dhcpv6 = this.section.getOption('dhcpv6').getUIElement(section_id),
+ ndp = this.section.getOption('ndp').getUIElement(section_id),
+ ra = this.section.getOption('ra').getUIElement(section_id);
+
+ /* Assume that serving RAs by default is fine, but disallow it for certain
+ interface protocols such as DHCP, DHCPv6 or the various PPP flavors.
+ The intent is to only allow RA serving for interface protocols doing
+ some kind of static IP config over something resembling a layer 2
+ ethernet device. */
+ switch (protoval) {
+ case 'dhcp':
+ case 'dhcpv6':
+ case '3g':
+ case 'l2tp':
+ case 'ppp':
+ case 'pppoa':
+ case 'pppoe':
+ case 'pptp':
+ case 'pppossh':
+ case 'ipip':
+ case 'gre':
+ case 'grev6':
+ ra_server_allowed = false;
+ break;
+ }
+
+ if (checked == '1' || !ra_server_allowed) {
+ dhcpv6.node.querySelector('li[data-value="server"]').setAttribute('unselectable', '');
+
+ if (dhcpv6.getValue() == 'server')
+ dhcpv6.setValue('hybrid');
+
+ ra.node.querySelector('li[data-value="server"]').setAttribute('unselectable', '');
+
+ if (ra.getValue() == 'server')
+ ra.setValue('hybrid');
+ }
+
+ if (checked == '1') {
+ dhcpv6.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_master_desc;
+ ra.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_master_desc;
+ ndp.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_master_desc;
+ }
+ else {
+ if (ra_server_allowed) {
+ dhcpv6.node.querySelector('li[data-value="server"]').removeAttribute('unselectable');
+ ra.node.querySelector('li[data-value="server"]').removeAttribute('unselectable');
+ }
+
+ dhcpv6.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_downstream_desc;
+ ra.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_downstream_desc;
+ ndp.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = ndp_downstream_desc ;
+ }
+
+ return true;
+ };
+
+
+ so = ss.taboption('ipv6', cbiRichListValue, 'ra', _('<abbr title="Router Advertisement">RA</abbr>-Service'),
+ _('Configures the operation mode of the <abbr title="Router Advertisement">RA</abbr> service on this interface.'));
+ so.value('', _('disabled'),
+ _('Do not send any <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages on this interface.'));
+ so.value('server', _('server mode'),
+ _('Send <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages advertising this device as IPv6 router.'));
+ so.value('relay', _('relay mode'),
+ _('Forward <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages received on the designated master interface to downstream interfaces.'));
+ so.value('hybrid', _('hybrid mode'), ' ');
+
+
+ so = ss.taboption('ipv6-ra', cbiRichListValue, 'ra_default', _('Default router'),
+ _('Configures the default router advertisement in <abbr title="Router Advertisement">RA</abbr> messages.'));
+ so.value('', _('automatic'),
+ _('Announce this device as default router if a local IPv6 default route is present.'));
+ so.value('1', _('on available prefix'),
+ _('Announce this device as default router if a public IPv6 prefix is available, regardless of local default route availability.'));
+ so.value('2', _('forced'),
+ _('Announce this device as default router regardless of whether a prefix or default route is present.'));
+ so.depends('ra', 'server');
+ so.depends({ ra: 'hybrid', master: '0' });
+
+ so = ss.taboption('ipv6-ra', form.Flag, 'ra_slaac', _('Enable <abbr title="Stateless Address Auto Config">SLAAC</abbr>'),
+ _('Set the autonomous address-configuration flag in the prefix information options of sent <abbr title="Router Advertisement">RA</abbr> messages. When enabled, clients will perform stateless IPv6 address autoconfiguration.'));
+ so.default = so.enabled;
+ so.depends('ra', 'server');
+ so.depends({ ra: 'hybrid', master: '0' });
+
+ so = ss.taboption('ipv6-ra', cbiRichListValue, 'ra_flags', _('<abbr title="Router Advertisement">RA</abbr> Flags'),
+ _('Specifies the flags sent in <abbr title="Router Advertisement">RA</abbr> messages, for example to instruct clients to request further information via stateful DHCPv6.'));
+ so.value('managed-config', _('managed config (M)'),
+ _('The <em>Managed address configuration</em> (M) flag indicates that IPv6 addresses are available via DHCPv6.'));
+ so.value('other-config', _('other config (O)'),
+ _('The <em>Other configuration</em> (O) flag indicates that other information, such as DNS servers, is available via DHCPv6.'));
+ so.value('home-agent', _('mobile home agent (H)'),
+ _('The <em>Mobile IPv6 Home Agent</em> (H) flag indicates that the device is also acting as Mobile IPv6 home agent on this link.'));
+ so.multiple = true;
+ so.select_placeholder = _('none');
+ so.depends('ra', 'server');
+ so.depends({ ra: 'hybrid', master: '0' });
+ so.cfgvalue = function(section_id) {
+ var flags = L.toArray(uci.get('dhcp', section_id, 'ra_flags'));
+ return flags.length ? flags : [ 'other-config' ];
+ };
+ so.remove = function(section_id) {
+ var existing = L.toArray(uci.get('dhcp', section_id, 'ra_flags'));
+ if (this.isActive(section_id)) {
+ if (existing.length != 1 || existing[0] != 'none')
+ uci.set('dhcp', section_id, 'ra_flags', [ 'none' ]);
+ }
+ else if (existing.length) {
+ uci.unset('dhcp', section_id, 'ra_flags');
+ }
+ };
+
+ so = ss.taboption('ipv6-ra', form.Value, 'ra_pref64', _('NAT64 prefix'), _('Announce NAT64 prefix in <abbr title="Router Advertisement">RA</abbr> messages.'));
so.optional = true;
- so.datatype = 'or(uinteger,ip4addr("nomask"))';
- so.default = '100';
+ so.datatype = 'cidr6';
+ so.placeholder = '64:ff9b::/96';
+ so.depends('ra', 'server');
+ so.depends({ ra: 'hybrid', master: '0' });
- so = ss.taboption('general', form.Value, 'limit', _('Limit'), _('Maximum number of leased addresses.'));
+ so = ss.taboption('ipv6-ra', form.Value, 'ra_maxinterval', _('Max <abbr title="Router Advertisement">RA</abbr> interval'), _('Maximum time allowed between sending unsolicited <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr>. Default is 600 seconds.'));
so.optional = true;
so.datatype = 'uinteger';
- so.default = '150';
+ so.placeholder = '600';
+ so.depends('ra', 'server');
+ so.depends({ ra: 'hybrid', master: '0' });
- so = ss.taboption('general', form.Value, 'leasetime', _('Lease time'), _('Expiry time of leased addresses, minimum is 2 minutes (<code>2m</code>).'));
+ so = ss.taboption('ipv6-ra', form.Value, 'ra_mininterval', _('Min <abbr title="Router Advertisement">RA</abbr> interval'), _('Minimum time allowed between sending unsolicited <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr>. Default is 200 seconds.'));
so.optional = true;
- so.default = '12h';
+ so.datatype = 'uinteger';
+ so.placeholder = '200';
+ so.depends('ra', 'server');
+ so.depends({ ra: 'hybrid', master: '0' });
- so = ss.taboption('advanced', form.Flag, 'dynamicdhcp', _('Dynamic <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>'), _('Dynamically allocate DHCP addresses for clients. If disabled, only clients having static leases will be served.'));
- so.default = so.enabled;
+ so = ss.taboption('ipv6-ra', form.Value, 'ra_lifetime', _('<abbr title="Router Advertisement">RA</abbr> Lifetime'), _('Router Lifetime published in <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages. Maximum is 9000 seconds.'));
+ so.optional = true;
+ so.datatype = 'range(0, 9000)';
+ so.placeholder = '1800';
+ so.depends('ra', 'server');
+ so.depends({ ra: 'hybrid', master: '0' });
- ss.taboption('advanced', form.Flag, 'force', _('Force'), _('Force DHCP on this network even if another server is detected.'));
+ so = ss.taboption('ipv6-ra', form.Value, 'ra_mtu', _('<abbr title="Router Advertisement">RA</abbr> MTU'), _('The <abbr title="Maximum Transmission Unit">MTU</abbr> to be published in <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages. Minimum is 1280 bytes.'));
+ so.optional = true;
+ so.datatype = 'range(1280, 65535)';
+ so.depends('ra', 'server');
+ so.depends({ ra: 'hybrid', master: '0' });
+ so.load = function(section_id) {
+ var dev = ifc.getL3Device(),
+ path = dev ? "/proc/sys/net/ipv6/conf/%s/mtu".format(dev.getName()) : null;
- // XXX: is this actually useful?
- //ss.taboption('advanced', form.Value, 'name', _('Name'), _('Define a name for this network.'));
+ return Promise.all([
+ dev ? L.resolveDefault(fs.read(path), dev.getMTU()) : null,
+ this.super('load', [section_id])
+ ]).then(L.bind(function(res) {
+ this.placeholder = +res[0];
- so = ss.taboption('advanced', form.Value, 'netmask', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Netmask'), _('Override the netmask sent to clients. Normally it is calculated from the subnet that is served.'));
+ return res[1];
+ }, this));
+ };
+
+ so = ss.taboption('ipv6-ra', form.Value, 'ra_hoplimit', _('<abbr title="Router Advertisement">RA</abbr> Hop Limit'), _('The maximum hops to be published in <abbr title="Router Advertisement">RA</abbr> messages. Maximum is 255 hops.'));
so.optional = true;
- so.datatype = 'ip4addr';
+ so.datatype = 'range(0, 255)';
+ so.depends('ra', 'server');
+ so.depends({ ra: 'hybrid', master: '0' });
+ so.load = function(section_id) {
+ var dev = ifc.getL3Device(),
+ path = dev ? "/proc/sys/net/ipv6/conf/%s/hop_limit".format(dev.getName()) : null;
- so.render = function(option_index, section_id, in_table) {
- this.placeholder = get_netmask(s, true);
- return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]);
- };
+ return Promise.all([
+ dev ? L.resolveDefault(fs.read(path), 64) : null,
+ this.super('load', [section_id])
+ ]).then(L.bind(function(res) {
+ this.placeholder = +res[0];
- so.validate = function(section_id, value) {
- var node = this.map.findElement('id', this.cbid(section_id));
- if (node)
- node.querySelector('input').setAttribute('placeholder', get_netmask(s, false));
- return form.Value.prototype.validate.apply(this, [ section_id, value ]);
+ return res[1];
+ }, this));
};
- ss.taboption('advanced', form.DynamicList, 'dhcp_option', _('DHCP-Options'), _('Define additional DHCP options, for example "<code>6,192.168.2.1,192.168.2.2</code>" which advertises different DNS servers to clients.'));
-
- for (var i = 0; i < ss.children.length; i++)
- if (ss.children[i].option != 'ignore')
- ss.children[i].depends('ignore', '0');
-
- so = ss.taboption('ipv6', form.ListValue, 'ra', _('Router Advertisement-Service'));
- so.value('', _('disabled'));
- so.value('server', _('server mode'));
- so.value('relay', _('relay mode'));
- so.value('hybrid', _('hybrid mode'));
-
- so = ss.taboption('ipv6', form.ListValue, 'dhcpv6', _('DHCPv6-Service'));
- so.value('', _('disabled'));
- so.value('server', _('server mode'));
- so.value('relay', _('relay mode'));
- so.value('hybrid', _('hybrid mode'));
-
- so = ss.taboption('ipv6', form.ListValue, 'ndp', _('NDP-Proxy'));
- so.value('', _('disabled'));
- so.value('relay', _('relay mode'));
- so.value('hybrid', _('hybrid mode'));
-
- so = ss.taboption('ipv6', form.Flag , 'master', _('Master'), _('Set this interface as master for the dhcpv6 relay.'));
- so.depends('dhcpv6', 'relay');
- so.depends('dhcpv6', 'hybrid');
-
- so = ss.taboption('ipv6', form.ListValue, 'ra_management', _('DHCPv6-Mode'), _('Default is stateless + stateful'));
- so.value('0', _('stateless'));
- so.value('1', _('stateless + stateful'));
- so.value('2', _('stateful-only'));
+
+ so = ss.taboption('ipv6', cbiRichListValue, 'dhcpv6', _('DHCPv6-Service'),
+ _('Configures the operation mode of the DHCPv6 service on this interface.'));
+ so.value('', _('disabled'),
+ _('Do not offer DHCPv6 service on this interface.'));
+ so.value('server', _('server mode'),
+ _('Provide a DHCPv6 server on this interface and reply to DHCPv6 solicitations and requests.'));
+ so.value('relay', _('relay mode'),
+ _('Forward DHCPv6 messages between the designated master interface and downstream interfaces.'));
+ so.value('hybrid', _('hybrid mode'), ' ');
+
+ so = ss.taboption('ipv6', form.Value, 'dhcpv6_pd_min_len', _('<abbr title="Prefix Delegation">PD</abbr> minimum length'),
+ _('Configures the minimum delegated prefix length assigned to a requesting downstream router, potentially overriding a requested prefix length. If left unspecified, the device will assign the smallest available prefix greater than or equal to the requested prefix.'));
+ so.datatype = 'range(1,62)';
so.depends('dhcpv6', 'server');
- so.depends('dhcpv6', 'hybrid');
- so.default = '1';
- so = ss.taboption('ipv6', form.Flag, 'ra_default', _('Always announce default router'), _('Announce as default router even if no public prefix is available.'));
+ so = ss.taboption('ipv6', form.DynamicList, 'dns', _('Announced IPv6 DNS servers'),
+ _('Specifies a fixed list of IPv6 DNS server addresses to announce via DHCPv6. If left unspecified, the device will announce itself as IPv6 DNS server unless the <em>Local IPv6 DNS server</em> option is disabled.'));
+ so.datatype = 'ip6addr("nomask")'; /* restrict to IPv6 only for now since dnsmasq (DHCPv4) does not honour this option */
+ so.depends('ra', 'server');
+ so.depends({ ra: 'hybrid', master: '0' });
+ so.depends('dhcpv6', 'server');
+ so.depends({ dhcpv6: 'hybrid', master: '0' });
+
+ so = ss.taboption('ipv6', form.Flag, 'dns_service', _('Local IPv6 DNS server'),
+ _('Announce this device as IPv6 DNS server.'));
+ so.default = so.enabled;
+ so.depends({ ra: 'server', dns: /^$/ });
+ so.depends({ ra: 'hybrid', dns: /^$/, master: '0' });
+ so.depends({ dhcpv6: 'server', dns: /^$/ });
+ so.depends({ dhcpv6: 'hybrid', dns: /^$/, master: '0' });
+
+ so = ss.taboption('ipv6', form.DynamicList, 'domain', _('Announced DNS domains'),
+ _('Specifies a fixed list of DNS search domains to announce via DHCPv6. If left unspecified, the local device DNS search domain will be announced.'));
+ so.datatype = 'hostname';
so.depends('ra', 'server');
- so.depends('ra', 'hybrid');
+ so.depends({ ra: 'hybrid', master: '0' });
+ so.depends('dhcpv6', 'server');
+ so.depends({ dhcpv6: 'hybrid', master: '0' });
+
+
+ so = ss.taboption('ipv6', cbiRichListValue, 'ndp', _('<abbr title="Neighbour Discovery Protocol">NDP</abbr>-Proxy'),
+ _('Configures the operation mode of the NDP proxy service on this interface.'));
+ so.value('', _('disabled'),
+ _('Do not proxy any <abbr title="Neighbour Discovery Protocol">NDP</abbr> packets.'));
+ so.value('relay', _('relay mode'),
+ _('Forward <abbr title="Neighbour Discovery Protocol">NDP</abbr> <abbr title="Neighbour Solicitation, Type 135">NS</abbr> and <abbr title="Neighbour Advertisement, Type 136">NA</abbr> messages between the designated master interface and downstream interfaces.'));
+ so.value('hybrid', _('hybrid mode'), ' ');
+
- ss.taboption('ipv6', form.DynamicList, 'dns', _('Announced DNS servers'));
- ss.taboption('ipv6', form.DynamicList, 'domain', _('Announced DNS domains'));
+ so = ss.taboption('ipv6', form.Flag, 'ndproxy_routing', _('Learn routes'), _('Set up routes for proxied IPv6 neighbours.'));
+ so.default = so.enabled;
+ so.depends('ndp', 'relay');
+ so.depends('ndp', 'hybrid');
+
+ so = ss.taboption('ipv6', form.Flag, 'ndproxy_slave', _('NDP-Proxy slave'), _('Set interface as NDP-Proxy external slave. Default is off.'));
+ so.depends({ ndp: 'relay', master: '0' });
+ so.depends({ ndp: 'hybrid', master: '0' });
+
+ so = ss.taboption('ipv6', form.Value, 'preferred_lifetime', _('IPv6 Prefix Lifetime'), _('Preferred lifetime for a prefix.'));
+ so.optional = true;
+ so.placeholder = '12h';
+ so.value('5m', _('5m (5 minutes)'));
+ so.value('3h', _('3h (3 hours)'));
+ so.value('12h', _('12h (12 hours - default)'));
+ so.value('7d', _('7d (7 days)'));
+
+ //This is a ra_* setting, but its placement is more logical/findable under IPv6 settings.
+ so = ss.taboption('ipv6', form.Flag, 'ra_useleasetime', _('Follow IPv4 Lifetime'), _('DHCPv4 <code>leasetime</code> is used as limit and preferred lifetime of the IPv6 prefix.'));
+ so.optional = true;
}
ifc.renderFormOptions(s);
+ // Common interface options
+ o = nettools.replaceOption(s, 'advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
+ o.default = o.enabled;
+
+ if (has_peerdns(protoval)) {
+ o = nettools.replaceOption(s, 'advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
+ o.default = o.enabled;
+ }
+
+ o = nettools.replaceOption(s, 'advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
+ if (has_peerdns(protoval))
+ o.depends('peerdns', '0');
+ o.datatype = 'ipaddr';
+
+ o = nettools.replaceOption(s, 'advanced', form.DynamicList, 'dns_search', _('DNS search domains'));
+ if (protoval != 'static')
+ o.depends('peerdns', '0');
+ o.datatype = 'hostname';
+
+ o = nettools.replaceOption(s, 'advanced', form.Value, 'dns_metric', _('DNS weight'), _('The DNS server entries in the local resolv.conf are primarily sorted by the weight specified here'));
+ o.datatype = 'uinteger';
+ o.placeholder = '0';
+
+ o = nettools.replaceOption(s, 'advanced', form.Value, 'metric', _('Use gateway metric'));
+ o.datatype = 'uinteger';
+ o.placeholder = '0';
+
+ o = nettools.replaceOption(s, 'advanced', form.Value, 'ip4table', _('Override IPv4 routing table'));
+ o.datatype = 'or(uinteger, string)';
+ for (var i = 0; i < rtTables.length; i++)
+ o.value(rtTables[i][1], '%s (%d)'.format(rtTables[i][1], rtTables[i][0]));
+
+ o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6table', _('Override IPv6 routing table'));
+ o.datatype = 'or(uinteger, string)';
+ for (var i = 0; i < rtTables.length; i++)
+ o.value(rtTables[i][1], '%s (%d)'.format(rtTables[i][1], rtTables[i][0]));
+
+ if (protoval == 'dhcpv6') {
+ o = nettools.replaceOption(s, 'advanced', form.Flag, 'sourcefilter', _('IPv6 source routing'), _('Automatically handle multiple uplink interfaces using source-based policy routing.'));
+ o.default = o.enabled;
+ }
+
+ o = nettools.replaceOption(s, 'advanced', form.Flag, 'delegate', _('Delegate IPv6 prefixes'), _('Enable downstream delegation of IPv6 prefixes available on this interface'));
+ o.default = o.enabled;
+
+ o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6assign', _('IPv6 assignment length'), _('Assign a part of given length of every public IPv6-prefix to this interface'));
+ o.value('', _('disabled'));
+ o.value('64');
+ o.datatype = 'max(128)';
+
+ o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6hint', _('IPv6 assignment hint'), _('Assign prefix parts using this hexadecimal subprefix ID for this interface.'));
+ o.placeholder = '0';
+ o.validate = function(section_id, value) {
+ if (value == null || value == '')
+ return true;
+
+ var n = parseInt(value, 16);
+
+ if (!/^(0x)?[0-9a-fA-F]+$/.test(value) || isNaN(n) || n >= 0xffffffff)
+ return _('Expecting a hexadecimal assignment hint');
+
+ return true;
+ };
+ for (var i = 33; i <= 64; i++)
+ o.depends('ip6assign', String(i));
+
+
+ o = nettools.replaceOption(s, 'advanced', form.DynamicList, 'ip6class', _('IPv6 prefix filter'), _('If set, downstream subnets are only allocated from the given IPv6 prefix classes.'));
+ o.value('local', 'local (%s)'.format(_('Local ULA')));
+
+ var prefixClasses = {};
+
+ this.networks.forEach(function(net) {
+ var prefixes = net._ubus('ipv6-prefix');
+ if (Array.isArray(prefixes)) {
+ prefixes.forEach(function(pfx) {
+ if (L.isObject(pfx) && typeof(pfx['class']) == 'string') {
+ prefixClasses[pfx['class']] = prefixClasses[pfx['class']] || {};
+ prefixClasses[pfx['class']][net.getName()] = true;
+ }
+ });
+ }
+ });
+
+ Object.keys(prefixClasses).sort().forEach(function(c) {
+ var networks = Object.keys(prefixClasses[c]).sort().join(', ');
+ o.value(c, (c != networks) ? '%s (%s)'.format(c, networks) : c);
+ });
+
+
+ o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6ifaceid', _('IPv6 suffix'), _("Optional. Allowed values: 'eui64', 'random', fixed value like '::1' or '::1:2'. When IPv6 prefix (like 'a:b:c:d::') is received from a delegating server, use the suffix (like '::1') to form the IPv6 address ('a:b:c:d::1') for the interface."));
+ o.datatype = 'ip6hostid';
+ o.placeholder = '::1';
+
+ o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6weight', _('IPv6 preference'), _('When delegating prefixes to multiple downstreams, interfaces with a higher preference value are considered first when allocating subnets.'));
+ o.datatype = 'uinteger';
+ o.placeholder = '0';
+
for (var i = 0; i < s.children.length; i++) {
o = s.children[i];
switch (o.option) {
case 'proto':
- case 'delegate':
case 'auto':
- case 'type':
- case 'stp':
- case 'igmp_snooping':
- case 'ifname_single':
- case 'ifname_multi':
case '_dhcp':
case '_zone':
case '_switch_proto':
case '_ifacestat_modal':
continue;
+ case 'igmp_snooping':
+ case 'stp':
+ case 'type':
+ case '_net_device':
+ var deps = [];
+ for (var j = 0; j < protocols.length; j++) {
+ if (!protocols[j].isVirtual()) {
+ if (o.deps.length)
+ for (var k = 0; k < o.deps.length; k++)
+ deps.push(Object.assign({ proto: protocols[j].getProtocol() }, o.deps[k]));
+ else
+ deps.push({ proto: protocols[j].getProtocol() });
+ }
+ }
+ o.deps = deps;
+ break;
+
default:
if (o.deps.length)
for (var j = 0; j < o.deps.length; j++)
@@ -687,17 +1130,31 @@ return view.extend({
o.depends('proto', protoval);
}
}
+
+ this.activeSection = s.section;
}, this));
};
+ s.handleModalCancel = function(/* ... */) {
+ var type = uci.get('network', this.activeSection || this.addedSection, 'type'),
+ device = (type == 'bridge') ? 'br-%s'.format(this.activeSection || this.addedSection) : null;
+
+ uci.sections('network', 'bridge-vlan', function(bvs) {
+ if (device != null && bvs.device == device)
+ uci.remove('network', bvs['.name']);
+ });
+
+ return form.GridSection.prototype.handleModalCancel.apply(this, arguments);
+ };
+
s.handleAdd = function(ev) {
var m2 = new form.Map('network'),
s2 = m2.section(form.NamedSection, '_new_'),
protocols = network.getProtocols(),
- proto, name, bridge, ifname_single, ifname_multi;
+ proto, name, device;
protocols.sort(function(a, b) {
- return a.getProtocol() > b.getProtocol();
+ return L.naturalCompare(a.getProtocol(), b.getProtocol());
});
s2.render = function() {
@@ -727,30 +1184,15 @@ return view.extend({
proto = s2.option(form.ListValue, 'proto', _('Protocol'));
proto.validate = name.validate;
- bridge = s2.option(form.Flag, 'type', _('Bridge interfaces'), _('Creates a bridge over specified interface(s)'));
- bridge.modalonly = true;
- bridge.disabled = '';
- bridge.enabled = 'bridge';
-
- ifname_single = s2.option(widgets.DeviceSelect, 'ifname_single', _('Interface'));
- ifname_single.noaliases = false;
- ifname_single.optional = false;
-
- ifname_multi = s2.option(widgets.DeviceSelect, 'ifname_multi', _('Interface'));
- ifname_multi.nobridges = true;
- ifname_multi.noaliases = true;
- ifname_multi.multiple = true;
- ifname_multi.optional = true;
- ifname_multi.display_size = 6;
+ device = s2.option(widgets.DeviceSelect, 'device', _('Device'));
+ device.noaliases = false;
+ device.optional = false;
for (var i = 0; i < protocols.length; i++) {
proto.value(protocols[i].getProtocol(), protocols[i].getI18n());
- if (!protocols[i].isVirtual()) {
- bridge.depends({ proto: protocols[i].getProtocol() });
- ifname_single.depends({ type: '', proto: protocols[i].getProtocol() });
- ifname_multi.depends({ type: 'bridge', proto: protocols[i].getProtocol() });
- }
+ if (!protocols[i].isVirtual())
+ device.depends('proto', protocols[i].getProtocol());
}
m2.render().then(L.bind(function(nodes) {
@@ -766,7 +1208,7 @@ return view.extend({
'click': ui.createHandlerFn(this, function(ev) {
var nameval = name.isValid('_new_') ? name.formvalue('_new_') : null,
protoval = proto.isValid('_new_') ? proto.formvalue('_new_') : null,
- protoclass = protoval ? network.getProtocol(protoval) : null;
+ protoclass = protoval ? network.getProtocol(protoval, nameval) : null;
if (nameval == null || protoval == null || nameval == '' || protoval == '')
return;
@@ -782,17 +1224,14 @@ return view.extend({
return m.save(function() {
var section_id = uci.add('network', 'interface', nameval);
- uci.set('network', section_id, 'proto', protoval);
+ protoclass.set('proto', protoval);
+ protoclass.addDevice(device.formvalue('_new_'));
- if (ifname_single.isActive('_new_')) {
- uci.set('network', section_id, 'ifname', ifname_single.formvalue('_new_'));
- }
- else if (ifname_multi.isActive('_new_')) {
- uci.set('network', section_id, 'type', 'bridge');
- uci.set('network', section_id, 'ifname', L.toArray(ifname_multi.formvalue('_new_')).join(' '));
- }
- }).then(L.bind(m.children[0].renderMoreOptionsModal, m.children[0], nameval));
+ m.children[0].addedSection = section_id;
+ ui.hideModal();
+ ui.showModal(null, E('p', { 'class': 'spinning' }, [ _('Loading data…') ]));
+ }).then(L.bind(m.children[0].renderMoreOptionsModal, m.children[0], nameval));
});
})
}, _('Create interface'))
@@ -821,9 +1260,9 @@ return view.extend({
var node = E('div', { 'class': 'ifacebox' }, [
E('div', {
'class': 'ifacebox-head',
- 'style': 'background-color:%s'.format(zone ? zone.getColor() : '#EEEEEE'),
+ 'style': firewall.getZoneColorStyle(zone),
'title': zone ? _('Part of zone %q').format(zone.getName()) : _('No zone assigned')
- }, E('strong', net.getName().toUpperCase())),
+ }, E('strong', net.getName())),
E('div', {
'class': 'ifacebox-body',
'id': '%s-ifc-devices'.format(section_id),
@@ -863,20 +1302,241 @@ return view.extend({
o = s.taboption('advanced', form.Flag, 'force_link', _('Force link'), _('Set interface properties regardless of the link carrier (If set, carrier sense events do not invoke hotplug handlers).'));
o.modalonly = true;
- o.render = function(option_index, section_id, in_table) {
- var protoopt = this.section.children.filter(function(o) { return o.option == 'proto' })[0],
- protoval = protoopt ? protoopt.cfgvalue(section_id) : null;
+ o.defaults = {
+ '1': [{ proto: 'static' }],
+ '0': []
+ };
+
+
+ // Device configuration
+ s = m.section(form.GridSection, 'device', _('Devices'));
+ s.addremove = true;
+ s.anonymous = true;
+ s.addbtntitle = _('Add device configuration…');
+
+ s.cfgsections = function() {
+ var sections = uci.sections('network', 'device'),
+ section_ids = sections.sort(function(a, b) { return L.naturalCompare(a.name, b.name) }).map(function(s) { return s['.name'] });
+
+ for (var i = 0; i < netDevs.length; i++) {
+ if (sections.filter(function(s) { return s.name == netDevs[i].getName() }).length)
+ continue;
+
+ if (netDevs[i].getType() == 'wifi' && !netDevs[i].isUp())
+ continue;
+
+ /* Unless http://lists.openwrt.org/pipermail/openwrt-devel/2020-July/030397.html is implemented,
+ we cannot properly redefine bridges as devices, so filter them away for now... */
+
+ var m = netDevs[i].isBridge() ? netDevs[i].getName().match(/^br-([A-Za-z0-9_]+)$/) : null,
+ s = m ? uci.get('network', m[1]) : null;
+
+ if (s && s['.type'] == 'interface' && s.type == 'bridge')
+ continue;
+
+ section_ids.push('dev:%s'.format(netDevs[i].getName()));
+ }
+
+ return section_ids;
+ };
+
+ s.renderMoreOptionsModal = function(section_id, ev) {
+ var m = section_id.match(/^dev:(.+)$/);
+
+ if (m) {
+ var devtype = getDevType(section_id);
+
+ section_id = uci.add('network', 'device');
- this.default = (protoval == 'static') ? this.enabled : this.disabled;
- return this.super('render', [ option_index, section_id, in_table ]);
+ uci.set('network', section_id, 'name', m[1]);
+ uci.set('network', section_id, 'type', (devtype != 'ethernet') ? devtype : null);
+
+ this.addedSection = section_id;
+ }
+
+ return this.super('renderMoreOptionsModal', [section_id, ev]);
+ };
+
+ s.renderRowActions = function(section_id) {
+ var trEl = this.super('renderRowActions', [ section_id, _('Configure…') ]),
+ deleteBtn = trEl.querySelector('button:last-child');
+
+ deleteBtn.firstChild.data = _('Unconfigure');
+ deleteBtn.setAttribute('title', _('Remove related device settings from the configuration'));
+ deleteBtn.disabled = section_id.match(/^dev:/) ? true : null;
+
+ return trEl;
+ };
+
+ s.modaltitle = function(section_id) {
+ var m = section_id.match(/^dev:(.+)$/),
+ name = m ? m[1] : uci.get('network', section_id, 'name');
+
+ return name ? '%s: %q'.format(getDevTypeDesc(section_id), name) : _('Add device configuration');
+ };
+
+ s.addModalOptions = function(s) {
+ var isNew = (uci.get('network', s.section, 'name') == null),
+ dev = getDevice(s.section);
+
+ nettools.addDeviceOptions(s, dev, isNew);
+ };
+
+ s.handleModalCancel = function(map /*, ... */) {
+ var name = uci.get('network', this.addedSection, 'name')
+
+ uci.sections('network', 'bridge-vlan', function(bvs) {
+ if (name != null && bvs.device == name)
+ uci.remove('network', bvs['.name']);
+ });
+
+ if (map.addedVLANs)
+ for (var i = 0; i < map.addedVLANs.length; i++)
+ uci.remove('network', map.addedVLANs[i]);
+
+ if (this.addedSection)
+ uci.remove('network', this.addedSection);
+
+ return form.GridSection.prototype.handleModalCancel.apply(this, arguments);
+ };
+
+ s.handleRemove = function(section_id /*, ... */) {
+ var name = uci.get('network', section_id, 'name'),
+ type = uci.get('network', section_id, 'type');
+
+ if (name != null && type == 'bridge') {
+ uci.sections('network', 'bridge-vlan', function(bvs) {
+ if (bvs.device == name)
+ uci.remove('network', bvs['.name']);
+ });
+ }
+
+ return form.GridSection.prototype.handleRemove.apply(this, arguments);
+ };
+
+ function getDevice(section_id) {
+ var m = section_id.match(/^dev:(.+)$/),
+ name = m ? m[1] : uci.get('network', section_id, 'name');
+
+ return netDevs.filter(function(d) { return d.getName() == name })[0];
+ }
+
+ function getDevType(section_id) {
+ var dev = getDevice(section_id),
+ cfg = uci.get('network', section_id),
+ type = cfg ? (uci.get('network', section_id, 'type') || 'ethernet') : (dev ? dev.getType() : '');
+
+ switch (type) {
+ case '':
+ return null;
+
+ case 'vlan':
+ case '8021q':
+ return '8021q';
+
+ case '8021ad':
+ return '8021ad';
+
+ case 'bridge':
+ return 'bridge';
+
+ case 'tunnel':
+ return 'tunnel';
+
+ case 'macvlan':
+ return 'macvlan';
+
+ case 'veth':
+ return 'veth';
+
+ case 'wifi':
+ case 'alias':
+ case 'switch':
+ case 'ethernet':
+ default:
+ return 'ethernet';
+ }
+ }
+
+ function getDevTypeDesc(section_id) {
+ switch (getDevType(section_id) || '') {
+ case '':
+ return E('em', [ _('Device not present') ]);
+
+ case '8021q':
+ return _('VLAN (802.1q)');
+
+ case '8021ad':
+ return _('VLAN (802.1ad)');
+
+ case 'bridge':
+ return _('Bridge device');
+
+ case 'tunnel':
+ return _('Tunnel device');
+
+ case 'macvlan':
+ return _('MAC VLAN');
+
+ case 'veth':
+ return _('Virtual Ethernet');
+
+ default:
+ return _('Network device');
+ }
+ }
+
+ o = s.option(form.DummyValue, 'name', _('Device'));
+ o.modalonly = false;
+ o.textvalue = function(section_id) {
+ var dev = getDevice(section_id),
+ ext = section_id.match(/^dev:/),
+ icon = render_iface(dev);
+
+ if (ext)
+ icon.querySelector('img').style.opacity = '.5';
+
+ return E('span', { 'class': 'ifacebadge' }, [
+ icon,
+ E('span', { 'style': ext ? 'opacity:.5' : null }, [
+ dev ? dev.getName() : (uci.get('network', section_id, 'name') || '?')
+ ])
+ ]);
+ };
+
+ o = s.option(form.DummyValue, 'type', _('Type'));
+ o.textvalue = getDevTypeDesc;
+ o.modalonly = false;
+
+ o = s.option(form.DummyValue, 'macaddr', _('MAC Address'));
+ o.modalonly = false;
+ o.textvalue = function(section_id) {
+ var dev = getDevice(section_id),
+ val = uci.get('network', section_id, 'macaddr'),
+ mac = dev ? dev.getMAC() : null;
+
+ return val ? E('strong', {
+ 'data-tooltip': _('The value is overridden by configuration.')
+ }, [ val.toUpperCase() ]) : (mac || '-');
};
+ o = s.option(form.DummyValue, 'mtu', _('MTU'));
+ o.modalonly = false;
+ o.textvalue = function(section_id) {
+ var dev = getDevice(section_id),
+ val = uci.get('network', section_id, 'mtu'),
+ mtu = dev ? dev.getMTU() : null;
+
+ return val ? E('strong', {
+ 'data-tooltip': _('The value is overridden by configuration.')
+ }, [ val ]) : (mtu || '-').toString();
+ };
s = m.section(form.TypedSection, 'globals', _('Global network options'));
s.addremove = false;
s.anonymous = true;
- o = s.option(form.Value, 'ula_prefix', _('IPv6 ULA-Prefix'));
+ o = s.option(form.Value, 'ula_prefix', _('IPv6 ULA-Prefix'), _('Unique Local Address - in the range <code>fc00::/7</code>. Typically only within the &#8216;local&#8217; half <code>fd00::/8</code>. ULA for IPv6 is analogous to IPv4 private network addressing. This prefix is randomly generated at first install.'));
o.datatype = 'cidr6';
o = s.option(form.Flag, 'packet_steering', _('Packet Steering'), _('Enable packet steering across all CPUs. May help or hinder network speed.'));
@@ -888,21 +1548,27 @@ return view.extend({
s.anonymous = true;
o = s.option(form.ListValue, 'annex', _('Annex'));
- o.value('a', _('Annex A + L + M (all)'));
- o.value('b', _('Annex B (all)'));
- o.value('j', _('Annex J (all)'));
- o.value('m', _('Annex M (all)'));
- o.value('bdmt', _('Annex B G.992.1'));
- o.value('b2', _('Annex B G.992.3'));
- o.value('b2p', _('Annex B G.992.5'));
+ if (dslModemType == 'vdsl') {
+ o.value('a', _('ADSL (all variants) Annex A/L/M + VDSL2 Annex A/B/C'));
+ o.value('b', _('ADSL (all variants) Annex B + VDSL2 Annex A/B/C'));
+ o.value('j', _('ADSL (all variants) Annex B/J + VDSL2 Annex A/B/C'));
+ } else {
+ o.value('a', _('ADSL (all variants) Annex A/L/M'));
+ o.value('b', _('ADSL (all variants) Annex B'));
+ o.value('j', _('ADSL (all variants) Annex B/J'));
+ }
+ o.value('m', _('ADSL (all variants) Annex M'));
o.value('at1', _('ANSI T1.413'));
- o.value('admt', _('Annex A G.992.1'));
- o.value('alite', _('Annex A G.992.2'));
- o.value('a2', _('Annex A G.992.3'));
- o.value('a2p', _('Annex A G.992.5'));
- o.value('l', _('Annex L G.992.3 POTS 1'));
- o.value('m2', _('Annex M G.992.3'));
- o.value('m2p', _('Annex M G.992.5'));
+ o.value('admt', _('ADSL (G.992.1) Annex A'));
+ o.value('bdmt', _('ADSL (G.992.1) Annex B'));
+ o.value('alite', _('Splitterless ADSL (G.992.2) Annex A'));
+ o.value('a2', _('ADSL2 (G.992.3) Annex A'));
+ o.value('b2', _('ADSL2 (G.992.3) Annex B'));
+ o.value('l', _('ADSL2 (G.992.3) Annex L'));
+ o.value('m2', _('ADSL2 (G.992.3) Annex M'));
+ o.value('a2p', _('ADSL2+ (G.992.5) Annex A'));
+ o.value('b2p', _('ADSL2+ (G.992.5) Annex B'));
+ o.value('m2p', _('ADSL2+ (G.992.5) Annex M'));
o = s.option(form.ListValue, 'tone', _('Tone'));
o.value('', _('auto'));
diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js
index b218daac34..4004be219a 100644
--- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js
+++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/routes.js
@@ -1,101 +1,207 @@
'use strict';
'require view';
+'require fs';
+'require uci';
'require form';
'require network';
'require tools.widgets as widgets';
return view.extend({
load: function() {
- return network.getDevices();
+ return Promise.all([
+ network.getDevices(),
+ fs.lines('/etc/iproute2/rt_tables')
+ ]);
},
- render: function(netdevs) {
- var m, s, o;
+ render: function(data) {
+ var netDevs = data[0],
+ m, s, o;
- m = new form.Map('network', _('Routes'), _('Routes specify over which interface and gateway a certain host or network can be reached.'));
+ var rtTables = data[1].map(function(l) {
+ var m = l.trim().match(/^(\d+)\s+(\S+)$/);
+ return m ? [ +m[1], m[2] ] : null;
+ }).filter(function(e) {
+ return e && e[0] > 0;
+ });
+
+ m = new form.Map('network', _('Routing'), _('Routing defines over which interface and gateway a certain host or network can be reached.'));
m.tabbed = true;
- for (var i = 4; i <= 6; i += 2) {
- s = m.section(form.GridSection, (i == 4) ? 'route' : 'route6', (i == 4) ? _('Static IPv4 Routes') : _('Static IPv6 Routes'));
+ for (var family = 4; family <= 6; family += 2) {
+ s = m.section(form.GridSection, (family == 6) ? 'route6' : 'route', (family == 6) ? _('Static IPv6 Routes') : _('Static IPv4 Routes'));
s.anonymous = true;
s.addremove = true;
s.sortable = true;
+ s.nodescriptions = true;
s.tab('general', _('General Settings'));
s.tab('advanced', _('Advanced Settings'));
- o = s.taboption('general', widgets.NetworkSelect, 'interface', _('Interface'));
- o.rmempty = false;
+ o = s.taboption('general', widgets.NetworkSelect, 'interface', _('Interface'), _('Specifies the logical interface name of the parent (or master) interface this route belongs to'));
+ o.loopback = true;
o.nocreate = true;
-
- o = s.taboption('general', form.Value, 'target', _('Target'), (i == 4) ? _('Host-<abbr title="Internet Protocol Address">IP</abbr> or Network') : _('<abbr title="Internet Protocol Version 6">IPv6</abbr>-Address or Network (CIDR)'));
- o.datatype = (i == 4) ? 'ip4addr' : 'ip6addr';
o.rmempty = false;
- if (i == 4) {
- o = s.taboption('general', form.Value, 'netmask', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Netmask'), _('if target is a network'));
- o.placeholder = '255.255.255.255';
- o.datatype = 'ip4addr';
- o.rmempty = true;
+ o = s.taboption('general', form.ListValue, 'type', _('Route type'), _('Specifies the route type to be created'));
+ o.modalonly = true;
+ o.value('', 'unicast');
+ o.value('local');
+ o.value('broadcast');
+ o.value('multicast');
+ o.value('unreachable');
+ o.value('prohibit');
+ o.value('blackhole');
+ o.value('anycast');
+ o.value('throw');
+
+ o = s.taboption('general', form.Value, 'target', _('Target'), _('Network address'));
+ o.rmempty = false;
+ o.datatype = (family == 6) ? 'cidr6' : 'cidr4';
+ o.placeholder = (family == 6) ? '::/0' : '0.0.0.0/0';
+ o.cfgvalue = function(section_id) {
+ var section_type = uci.get('network', section_id, '.type'),
+ target = uci.get('network', section_id, 'target'),
+ mask = uci.get('network', section_id, 'netmask'),
+ v6 = (section_type == 'route6') ? true : false,
+ bits = mask ? network.maskToPrefix(mask, v6) : (v6 ? 128 : 32);
+ if (target) {
+ return target.split('/')[1] ? target : target + '/' + bits;
+ }
+ }
+ o.write = function(section_id, formvalue) {
+ uci.set('network', section_id, 'target', formvalue);
+ uci.unset('network', section_id, 'netmask');
}
- o = s.taboption('general', form.Value, 'gateway', (i == 4) ? _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Gateway') : _('<abbr title="Internet Protocol Version 6">IPv6</abbr>-Gateway'));
- o.datatype = (i == 4) ? 'ip4addr' : 'ip6addr';
- o.rmempty = true;
+ o = s.taboption('general', form.Value, 'gateway', _('Gateway'), _('Specifies the network gateway. If omitted, the gateway from the parent interface is taken if any, otherwise creates a link scope route. If set to 0.0.0.0 no gateway will be specified for the route'));
+ o.datatype = (family == 6) ? 'ip6addr("nomask")' : 'ip4addr("nomask")';
+ o.placeholder = (family == 6) ? 'fe80::1' : '192.168.0.1';
- o = s.taboption('advanced', form.Value, 'metric', _('Metric'));
+ o = s.taboption('advanced', form.Value, 'metric', _('Metric'), _('Specifies the route metric to use'));
+ o.datatype = 'uinteger';
o.placeholder = 0;
- o.datatype = (i == 4) ? 'range(0,255)' : 'range(0,65535)';
- o.rmempty = true;
o.textvalue = function(section_id) {
- return this.cfgvalue(section_id) || 0;
+ return this.cfgvalue(section_id) || E('em', _('auto'));
};
- o = s.taboption('advanced', form.Value, 'mtu', _('MTU'));
+ o = s.taboption('advanced', form.Value, 'mtu', _('MTU'), _('Defines a specific MTU for this route'));
+ o.modalonly = true;
+ o.datatype = 'and(uinteger,range(64,9000))';
o.placeholder = 1500;
- o.datatype = 'range(64,9000)';
- o.rmempty = true;
+
+ o = s.taboption('advanced', form.Value, 'table', _('Table'), _('The rule target is a table lookup ID: a numeric table index ranging from 0 to 65535 or symbol alias declared in /etc/iproute2/rt_tables. Special aliases local (255), main (254) and default (253) are also valid'));
+ o.datatype = 'or(uinteger, string)';
+ for (var i = 0; i < rtTables.length; i++)
+ o.value(rtTables[i][1], '%s (%d)'.format(rtTables[i][1], rtTables[i][0]));
+ o.textvalue = function(section_id) {
+ return this.cfgvalue(section_id) || E('em', _('auto'));
+ };
+
+ o = s.taboption('advanced', form.Value, 'source', _('Source'), _('Specifies the preferred source address when sending to destinations covered by the target'));
o.modalonly = true;
+ o.datatype = (family == 6) ? 'ip6addr' : 'ip4addr';
+ for (var i = 0; i < netDevs.length; i++) {
+ var addrs = (family == 6) ? netDevs[i].getIP6Addrs() : netDevs[i].getIPAddrs();
+ for (var j = 0; j < addrs.length; j++)
+ o.value(addrs[j].split('/')[0]);
+ }
+
+ o = s.taboption('advanced', form.Flag, 'onlink', _('On-link'), _('When enabled, gateway is on-link even if the gateway does not match any interface prefix'));
+ o.modalonly = true;
+ o.default = o.disabled;
- o = s.taboption('advanced', form.ListValue, 'type', _('Route type'));
+ o = s.taboption('advanced', form.Flag, 'disabled', _('Disable'));
+ o.modalonly = false;
+ o.editable = true;
+ o.default = o.disabled;
+ }
+
+ for (var family = 4; family <= 6; family += 2) {
+ s = m.section(form.GridSection, (family == 6) ? 'rule6' : 'rule', (family == 6) ? _('IPv6 Rules') : _('IPv4 Rules'));
+ s.anonymous = true;
+ s.addremove = true;
+ s.sortable = true;
+ s.nodescriptions = true;
+
+ s.tab('general', _('General Settings'));
+ s.tab('advanced', _('Advanced Settings'));
+
+ o = s.taboption('general', form.Value, 'priority', _('Priority'), _('Specifies the ordering of the IP rules'));
+ o.datatype = 'uinteger';
+ o.placeholder = 30000;
+ o.textvalue = function(section_id) {
+ return this.cfgvalue(section_id) || E('em', _('auto'));
+ };
+
+ o = s.taboption('general', form.ListValue, 'action', _('Rule type'), _('Specifies the rule target routing action'));
+ o.modalonly = true;
o.value('', 'unicast');
- o.value('local');
- o.value('broadcast');
- o.value('multicast');
o.value('unreachable');
o.value('prohibit');
o.value('blackhole');
- o.value('anycast');
- o.default = '';
- o.rmempty = true;
+ o.value('throw');
+
+ o = s.taboption('general', widgets.NetworkSelect, 'in', _('Incoming interface'), _('Specifies the incoming logical interface name'));
+ o.loopback = true;
+ o.nocreate = true;
+
+ o = s.taboption('general', form.Value, 'src', _('Source'), _('Specifies the source subnet to match (CIDR notation)'));
+ o.datatype = (family == 6) ? 'cidr6' : 'cidr4';
+ o.placeholder = (family == 6) ? '::/0' : '0.0.0.0/0';
+ o.textvalue = function(section_id) {
+ return this.cfgvalue(section_id) || E('em', _('any'));
+ };
+
+ o = s.taboption('general', widgets.NetworkSelect, 'out', _('Outgoing interface'), _('Specifies the outgoing logical interface name'));
+ o.loopback = true;
+ o.nocreate = true;
+
+ o = s.taboption('general', form.Value, 'dest', _('Destination'), _('Specifies the destination subnet to match (CIDR notation)'));
+ o.datatype = (family == 6) ? 'cidr6' : 'cidr4';
+ o.placeholder = (family == 6) ? '::/0' : '0.0.0.0/0';
+ o.textvalue = function(section_id) {
+ return this.cfgvalue(section_id) || E('em', _('any'));
+ };
+
+ o = s.taboption('general', form.Value, 'lookup', _('Table'), _('The rule target is a table lookup ID: a numeric table index ranging from 0 to 65535 or symbol alias declared in /etc/iproute2/rt_tables. Special aliases local (255), main (254) and default (253) are also valid'));
+ o.datatype = 'or(uinteger, string)';
+ for (var i = 0; i < rtTables.length; i++)
+ o.value(rtTables[i][1], '%s (%d)'.format(rtTables[i][1], rtTables[i][0]));
+
+ o = s.taboption('advanced', form.Value, 'goto', _('Jump to rule'), _('The rule target is a jump to another rule specified by its priority value'));
o.modalonly = true;
+ o.datatype = 'uinteger';
+ o.placeholder = 80000;
- o = s.taboption('advanced', form.Value, 'table', _('Route table'));
- o.value('local', 'local (255)');
- o.value('main', 'main (254)');
- o.value('default', 'default (253)');
- o.rmempty = true;
+ o = s.taboption('advanced', form.Value, 'mark', _('Firewall mark'), _('Specifies the fwmark and optionally its mask to match, e.g. 0xFF to match mark 255 or 0x0/0x1 to match any even mark value'));
o.modalonly = true;
- o.cfgvalue = function(section_id) {
- var cfgvalue = this.map.data.get('network', section_id, 'table');
- return cfgvalue || 'main';
- };
+ o.datatype = 'string';
+ o.placeholder = '0x1/0xf';
- o = s.taboption('advanced', form.Value, 'source', _('Source Address'));
- o.placeholder = E('em', _('automatic'));
- for (var j = 0; j < netdevs.length; j++) {
- var addrs = netdevs[j].getIPAddrs();
- for (var k = 0; k < addrs.length; k++)
- o.value(addrs[k].split('/')[0]);
- }
- o.datatype = (i == 4) ? 'ip4addr' : 'ip6addr';
- o.default = '';
- o.rmempty = true;
+ o = s.taboption('advanced', form.Value, 'tos', _('Type of service'), _('Specifies the TOS value to match in IP headers'));
+ o.modalonly = true;
+ o.datatype = 'uinteger';
+ o.placeholder = 10;
+
+ o = s.taboption('advanced', form.Value, 'uidrange', _('User identifier'), _('Specifies an individual UID or range of UIDs to match, e.g. 1000 to match corresponding UID or 1000-1005 to inclusively match all UIDs within the corresponding range'));
+ o.modalonly = true;
+ o.datatype = 'string';
+ o.placeholder = '1000-1005';
+
+ o = s.taboption('advanced', form.Value, 'suppress_prefixlength', _('Prefix suppressor'), _('Reject routing decisions that have a prefix length less than or equal to the specified value'));
+ o.modalonly = true;
+ o.datatype = (family == 6) ? 'ip6prefix' : 'ip4prefix';
+ o.placeholder = (family == 6) ? 64 : 24;
+
+ o = s.taboption('advanced', form.Flag, 'invert', _('Invert match'), _('If set, the meaning of the match options is inverted'));
o.modalonly = true;
+ o.default = o.disabled;
- o = s.taboption('advanced', form.Flag, 'onlink', _('On-Link route'));
+ o = s.taboption('advanced', form.Flag, 'disabled', _('Disable'));
+ o.modalonly = false;
+ o.editable = true;
o.default = o.disabled;
- o.rmempty = true;
}
return m.render();
diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js
index 3133d27250..535a133e78 100644
--- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js
+++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js
@@ -180,8 +180,10 @@ return view.extend({
s = m.section(form.NamedSection, sid, 'switch', switch_title);
s.addremove = false;
- if (feat.vlan_option)
- s.option(form.Flag, feat.vlan_option, _('Enable VLAN functionality'));
+ if (feat.vlan_option) {
+ o = s.option(form.Flag, feat.vlan_option, _('Enable VLAN functionality'));
+ o.rmempty = false;
+ }
if (feat.learning_option) {
o = s.option(form.Flag, feat.learning_option, _('Enable learning and aging'));
@@ -222,7 +224,7 @@ return view.extend({
s.filter = function(section_id) {
var device = uci.get('network', section_id, 'device');
- return (device == switch_name);
+ return (device == this.device);
};
s.cfgsections = function() {
@@ -246,7 +248,7 @@ return view.extend({
max_vid = 0;
for (var j = 0; j < sections.length; j++) {
- if (sections[j].device != s.device)
+ if (sections[j].device != this.device)
continue;
var vlan = +sections[j].vlan,
@@ -259,7 +261,7 @@ return view.extend({
max_vid = vid;
}
- uci.set('network', section_id, 'device', s.device);
+ uci.set('network', section_id, 'device', this.device);
uci.set('network', section_id, 'vlan', max_vlan + 1);
if (feat.vid_option)
@@ -268,8 +270,6 @@ return view.extend({
return this.map.save(null, true);
};
- var port_opts = [];
-
o = s.option(form.Value, feat.vid_option || 'vlan', 'VLAN ID');
o.rmempty = false;
o.forcewrite = true;
@@ -297,21 +297,23 @@ return view.extend({
return true;
};
+ var port_opts = o.port_opts = [];
+
o.write = function(section_id, value) {
var topology = this.section.topology,
values = [];
- for (var i = 0; i < port_opts.length; i++) {
- var tagging = port_opts[i].formvalue(section_id),
+ for (var i = 0; i < this.port_opts.length; i++) {
+ var tagging = this.port_opts[i].formvalue(section_id),
portspec = Array.isArray(topology.ports) ? topology.ports[i] : null;
if (tagging == 't')
- values.push(port_opts[i].option + tagging);
+ values.push(this.port_opts[i].option + tagging);
else if (tagging == 'u')
- values.push(port_opts[i].option);
+ values.push(this.port_opts[i].option);
if (portspec && portspec.device) {
- var old_tag = port_opts[i].cfgvalue(section_id),
+ var old_tag = this.port_opts[i].cfgvalue(section_id),
old_vid = this.cfgvalue(section_id);
if (old_tag != tagging || old_vid != value) {
diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js
index dc75c9509f..4c2daf588a 100644
--- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js
+++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js
@@ -199,7 +199,9 @@ function format_wifirate(rate) {
var s = '%.1f\xa0%s, %d\xa0%s'.format(rate.rate / 1000, _('Mbit/s'), rate.mhz, _('MHz')),
ht = rate.ht, vht = rate.vht,
mhz = rate.mhz, nss = rate.nss,
- mcs = rate.mcs, sgi = rate.short_gi;
+ mcs = rate.mcs, sgi = rate.short_gi,
+ he = rate.he, he_gi = rate.he_gi,
+ he_dcm = rate.he_dcm;
if (ht || vht) {
if (vht) s += ', VHT-MCS\xa0%d'.format(mcs);
@@ -208,6 +210,13 @@ function format_wifirate(rate) {
if (sgi) s += ', ' + _('Short GI').replace(/ /g, '\xa0');
}
+ if (he) {
+ s += ', HE-MCS\xa0%d'.format(mcs);
+ if (nss) s += ', HE-NSS\xa0%d'.format(nss);
+ if (he_gi) s += ', HE-GI\xa0%d'.format(he_gi);
+ if (he_dcm) s += ', HE-DCM\xa0%d'.format(he_dcm);
+ }
+
return s;
}
@@ -303,24 +312,33 @@ var CBIWifiFrequencyValue = form.Value.extend({
this.callFrequencyList(section_id)
]).then(L.bind(function(data) {
this.channels = {
- '11g': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', true ] : [],
- '11a': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', true ] : []
+ '2g': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', true ] : [],
+ '5g': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', true ] : [],
+ '6g': L.hasSystemFeature('hostapd', 'acs') ? [ 'auto', 'auto', true ] : [],
+ '60g': []
};
- for (var i = 0; i < data[1].length; i++)
- this.channels[(data[1][i].mhz > 2484) ? '11a' : '11g'].push(
+ for (var i = 0; i < data[1].length; i++) {
+ if (!data[1][i].band)
+ continue;
+
+ var band = '%dg'.format(data[1][i].band);
+
+ this.channels[band].push(
data[1][i].channel,
'%d (%d Mhz)'.format(data[1][i].channel, data[1][i].mhz),
!data[1][i].restricted
);
+ }
var hwmodelist = L.toArray(data[0] ? data[0].getHWModes() : null)
.reduce(function(o, v) { o[v] = true; return o }, {});
this.modes = [
- '', 'Legacy', true,
+ '', 'Legacy', hwmodelist.a || hwmodelist.b || hwmodelist.g,
'n', 'N', hwmodelist.n,
- 'ac', 'AC', hwmodelist.ac
+ 'ac', 'AC', L.hasSystemFeature('hostapd', '11ac') && hwmodelist.ac,
+ 'ax', 'AX', L.hasSystemFeature('hostapd', '11ax') && hwmodelist.ax
];
var htmodelist = L.toArray(data[0] ? data[0].getHTModes() : null)
@@ -337,20 +355,32 @@ var CBIWifiFrequencyValue = form.Value.extend({
'VHT40', '40 MHz', htmodelist.VHT40,
'VHT80', '80 MHz', htmodelist.VHT80,
'VHT160', '160 MHz', htmodelist.VHT160
+ ],
+ 'ax': [
+ 'HE20', '20 MHz', htmodelist.HE20,
+ 'HE40', '40 MHz', htmodelist.HE40,
+ 'HE80', '80 MHz', htmodelist.HE80,
+ 'HE160', '160 MHz', htmodelist.HE160
]
};
this.bands = {
'': [
- '11g', '2.4 GHz', this.channels['11g'].length > 3,
- '11a', '5 GHz', this.channels['11a'].length > 3
+ '2g', '2.4 GHz', this.channels['2g'].length > 3,
+ '5g', '5 GHz', this.channels['5g'].length > 3,
+ '60g', '60 GHz', this.channels['60g'].length > 0
],
'n': [
- '11g', '2.4 GHz', this.channels['11g'].length > 3,
- '11a', '5 GHz', this.channels['11a'].length > 3
+ '2g', '2.4 GHz', this.channels['2g'].length > 3,
+ '5g', '5 GHz', this.channels['5g'].length > 3
],
'ac': [
- '11a', '5 GHz', true
+ '5g', '5 GHz', true
+ ],
+ 'ax': [
+ '2g', '2.4 GHz', this.channels['2g'].length > 3,
+ '5g', '5 GHz', this.channels['5g'].length > 3,
+ '6g', '6 GHz', this.channels['6g'].length > 3
]
};
}, this));
@@ -392,6 +422,8 @@ var CBIWifiFrequencyValue = form.Value.extend({
this.setValues(band, this.bands[mode.value]);
this.toggleWifiChannel(elem);
+
+ this.map.checkDepends();
},
toggleWifiChannel: function(elem) {
@@ -408,11 +440,14 @@ var CBIWifiFrequencyValue = form.Value.extend({
bwdt = elem.querySelector('.htmode'),
htval = uci.get('wireless', section_id, 'htmode'),
hwval = uci.get('wireless', section_id, 'hwmode'),
- chval = uci.get('wireless', section_id, 'channel');
+ chval = uci.get('wireless', section_id, 'channel'),
+ bandval = uci.get('wireless', section_id, 'band');
this.setValues(mode, this.modes);
- if (/VHT20|VHT40|VHT80|VHT160/.test(htval))
+ if (/HE20|HE40|HE80|HE160/.test(htval))
+ mode.value = 'ax';
+ else if (/VHT20|VHT40|VHT80|VHT160/.test(htval))
mode.value = 'ac';
else if (/HT20|HT40/.test(htval))
mode.value = 'n';
@@ -421,15 +456,24 @@ var CBIWifiFrequencyValue = form.Value.extend({
this.toggleWifiMode(elem);
- if (/a/.test(hwval))
- band.value = '11a';
- else
- band.value = '11g';
+ if (hwval != null) {
+ this.useBandOption = false;
+
+ if (/a/.test(hwval))
+ band.value = '5g';
+ else
+ band.value = '2g';
+ }
+ else {
+ this.useBandOption = true;
+
+ band.value = bandval;
+ }
this.toggleWifiBand(elem);
bwdt.value = htval;
- chan.value = chval;
+ chan.value = chval || (chan.options[0] ? chan.options[0].value : 'auto');
return elem;
},
@@ -461,6 +505,7 @@ var CBIWifiFrequencyValue = form.Value.extend({
E('select', {
'class': 'channel',
'style': 'width:auto',
+ 'change': L.bind(this.map.checkDepends, this.map),
'disabled': (this.disabled != null) ? this.disabled : this.map.readonly
})
]),
@@ -469,6 +514,7 @@ var CBIWifiFrequencyValue = form.Value.extend({
E('select', {
'class': 'htmode',
'style': 'width:auto',
+ 'change': L.bind(this.map.checkDepends, this.map),
'disabled': (this.disabled != null) ? this.disabled : this.map.readonly
})
]),
@@ -481,7 +527,7 @@ var CBIWifiFrequencyValue = form.Value.extend({
cfgvalue: function(section_id) {
return [
uci.get('wireless', section_id, 'htmode'),
- uci.get('wireless', section_id, 'hwmode'),
+ uci.get('wireless', section_id, 'hwmode') || uci.get('wireless', section_id, 'band'),
uci.get('wireless', section_id, 'channel')
];
},
@@ -498,7 +544,12 @@ var CBIWifiFrequencyValue = form.Value.extend({
write: function(section_id, value) {
uci.set('wireless', section_id, 'htmode', value[0] || null);
- uci.set('wireless', section_id, 'hwmode', value[1]);
+
+ if (this.useBandOption)
+ uci.set('wireless', section_id, 'band', value[1]);
+ else
+ uci.set('wireless', section_id, 'hwmode', (value[1] == '2g') ? '11g' : '11a');
+
uci.set('wireless', section_id, 'channel', value[2]);
}
});
@@ -649,7 +700,7 @@ return view.extend({
if (bss.network.isClientDisconnectSupported()) {
if (table.firstElementChild.childNodes.length < 6)
- table.firstElementChild.appendChild(E('div', { 'class': 'th cbi-section-actions'}));
+ table.firstElementChild.appendChild(E('th', { 'class': 'th cbi-section-actions'}));
row.push(E('button', {
'class': 'cbi-button cbi-button-remove',
@@ -684,7 +735,8 @@ return view.extend({
load: function() {
return Promise.all([
uci.changes(),
- uci.load('wireless')
+ uci.load('wireless'),
+ uci.load('system')
]);
},
@@ -850,7 +902,7 @@ return view.extend({
];
}
- return E('div', { 'class': 'td middle cbi-section-actions' }, E('div', btns));
+ return E('td', { 'class': 'td middle cbi-section-actions' }, E('div', btns));
};
s.addModalOptions = function(s) {
@@ -883,14 +935,20 @@ return view.extend({
o.ucisection = s.section;
if (hwtype == 'mac80211') {
+ o = ss.taboption('general', form.Flag, 'legacy_rates', _('Allow legacy 802.11b rates'), _('Legacy or badly behaving devices may require legacy 802.11b rates to interoperate. Airtime efficiency may be significantly reduced where these are used. It is recommended to not allow 802.11b rates where possible.'));
+ o.depends({'_freq': '2g', '!contains': true});
+
o = ss.taboption('general', CBIWifiTxPowerValue, 'txpower', _('Maximum transmit power'), _('Specifies the maximum transmit power the wireless radio may use. Depending on regulatory requirements and wireless usage, the actual transmit power may be reduced by the driver.'));
o.wifiNetwork = radioNet;
o = ss.taboption('advanced', CBIWifiCountryValue, 'country', _('Country Code'));
o.wifiNetwork = radioNet;
- o = ss.taboption('advanced', form.Flag, 'legacy_rates', _('Allow legacy 802.11b rates'));
- o.default = o.enabled;
+ o = ss.taboption('advanced', form.ListValue, 'cell_density', _('Coverage cell density'), _('Configures data rates based on the coverage cell density. Normal configures basic rates to 6, 12, 24 Mbps if legacy 802.11b rates are not used else to 5.5, 11 Mbps. High configures basic rates to 12, 24 Mbps if legacy 802.11b rates are not used else to the 11 Mbps rate. Very High configures 24 Mbps as the basic rate. Supported rates lower than the minimum basic rate are not offered.'));
+ o.value('0', _('Disabled'));
+ o.value('1', _('Normal'));
+ o.value('2', _('High'));
+ o.value('3', _('Very High'));
o = ss.taboption('advanced', form.Value, 'distance', _('Distance Optimization'), _('Distance to farthest network member in meters.'));
o.datatype = 'or(range(0,114750),"auto")';
@@ -922,6 +980,7 @@ return view.extend({
ss.tab('encryption', _('Wireless Security'));
ss.tab('macfilter', _('MAC-Filter'));
ss.tab('advanced', _('Advanced Settings'));
+ ss.tab('roaming', _('WLAN roaming'), _('Settings for assisting wireless clients in roaming between multiple APs: 802.11r, 802.11k and 802.11v'));
o = ss.taboption('general', form.ListValue, 'mode', _('Mode'));
o.value('ap', _('Access Point'));
@@ -977,8 +1036,17 @@ return view.extend({
return net || network.addNetwork(name, { proto: 'none' });
}, this, values[i])).then(L.bind(function(dev, net) {
if (net) {
- if (!net.isEmpty())
- net.set('type', 'bridge');
+ if (!net.isEmpty()) {
+ var target_dev = net.getDevice();
+
+ /* Resolve parent interface of vlan */
+ while (target_dev && target_dev.getType() == 'vlan')
+ target_dev = target_dev.getParent();
+
+ if (!target_dev || target_dev.getType() != 'bridge')
+ net.set('type', 'bridge');
+ }
+
net.addDevice(dev);
}
}, this, dev)));
@@ -1008,7 +1076,7 @@ return view.extend({
bssid.depends('mode', 'sta');
bssid.depends('mode', 'sta-wds');
- o = ss.taboption('macfilter', form.ListValue, 'macfilter', _('MAC-Address Filter'));
+ o = ss.taboption('macfilter', form.ListValue, 'macfilter', _('MAC Address Filter'));
o.depends('mode', 'ap');
o.depends('mode', 'ap-wds');
o.value('', _('disable'));
@@ -1017,6 +1085,7 @@ return view.extend({
o = ss.taboption('macfilter', form.DynamicList, 'maclist', _('MAC-List'));
o.datatype = 'macaddr';
+ o.retain = true;
o.depends('macfilter', 'allow');
o.depends('macfilter', 'deny');
o.load = function(section_id) {
@@ -1063,25 +1132,37 @@ return view.extend({
return mode;
};
- o = ss.taboption('general', form.Flag, 'hidden', _('Hide <abbr title="Extended Service Set Identifier">ESSID</abbr>'));
+ o = ss.taboption('general', form.Flag, 'hidden', _('Hide <abbr title="Extended Service Set Identifier">ESSID</abbr>'), _('Where the ESSID is hidden, clients may fail to roam and airtime efficiency may be significantly reduced.'));
o.depends('mode', 'ap');
o.depends('mode', 'ap-wds');
- o = ss.taboption('general', form.Flag, 'wmm', _('WMM Mode'));
+ o = ss.taboption('general', form.Flag, 'wmm', _('WMM Mode'), _('Where Wi-Fi Multimedia (WMM) Mode QoS is disabled, clients may be limited to 802.11a/802.11g rates.'));
o.depends('mode', 'ap');
o.depends('mode', 'ap-wds');
o.default = o.enabled;
+ /* https://w1.fi/cgit/hostap/commit/?id=34f7c699a6bcb5c45f82ceb6743354ad79296078 */
+ /* multicast_to_unicast https://github.com/openwrt/openwrt/commit/7babb978ad9d7fc29acb1ff86afb1eb343af303a */
+ o = ss.taboption('advanced', form.Flag, 'multicast_to_unicast_all', _('Multi To Unicast'), _('ARP, IPv4 and IPv6 (even 802.1Q) with multicast destination MACs are unicast to the STA MAC address. Note: This is not Directed Multicast Service (DMS) in 802.11v. Note: might break receiver STA multicast expectations.'));
+ o.rmempty = true;
+
o = ss.taboption('advanced', form.Flag, 'isolate', _('Isolate Clients'), _('Prevents client-to-client communication'));
o.depends('mode', 'ap');
o.depends('mode', 'ap-wds');
o = ss.taboption('advanced', form.Value, 'ifname', _('Interface name'), _('Override default interface name'));
o.optional = true;
+ o.datatype = 'netdevname';
o.placeholder = radioNet.getIfname();
if (/^radio\d+\.network/.test(o.placeholder))
o.placeholder = '';
+ var macaddr = uci.get('wireless', radioNet.getName(), 'macaddr');
+ o = ss.taboption('advanced', form.Value, 'macaddr', _('MAC address'), _('Override default MAC address - the range of usable addresses might be limited by the driver'));
+ o.value('', _('driver default (%s)').format(!macaddr ? radioNet.getActiveBSSID() : _('no override')));
+ o.value('random', _('randomly generated'));
+ o.datatype = "or('random',macaddr)";
+
o = ss.taboption('advanced', form.Flag, 'short_preamble', _('Short Preamble'));
o.default = o.enabled;
@@ -1099,7 +1180,7 @@ return view.extend({
o.optional = true;
o.datatype = 'uinteger';
- o = ss.taboption('advanced', form.Value, 'max_inactivity', _('Station inactivity limit'), _('sec'));
+ o = ss.taboption('advanced', form.Value, 'max_inactivity', _('Station inactivity limit'), _('802.11v: BSS Max Idle. Units: seconds.'));
o.optional = true;
o.placeholder = 300;
o.datatype = 'uinteger';
@@ -1202,7 +1283,7 @@ return view.extend({
if (has_hostapd || has_supplicant) {
crypto_modes.push(['psk2', 'WPA2-PSK', 35]);
crypto_modes.push(['psk-mixed', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]);
- crypto_modes.push(['psk', 'WPA-PSK', 21]);
+ crypto_modes.push(['psk', 'WPA-PSK', 12]);
}
else {
encr.description = _('WPA-Encryption requires wpa_supplicant (for client mode) or hostapd (for AP and ad-hoc mode) to be installed.');
@@ -1302,7 +1383,7 @@ return view.extend({
else if (hwtype == 'broadcom') {
crypto_modes.push(['psk2', 'WPA2-PSK', 33]);
crypto_modes.push(['psk+psk2', 'WPA-PSK/WPA2-PSK Mixed Mode', 22]);
- crypto_modes.push(['psk', 'WPA-PSK', 21]);
+ crypto_modes.push(['psk', 'WPA-PSK', 12]);
crypto_modes.push(['wep-open', _('WEP Open System'), 11]);
crypto_modes.push(['wep-shared', _('WEP Shared Key'), 10]);
}
@@ -1320,51 +1401,89 @@ return view.extend({
}
- o = ss.taboption('encryption', form.Value, 'auth_server', _('Radius-Authentication-Server'));
+ o = ss.taboption('encryption', form.Value, 'auth_server', _('RADIUS Authentication Server'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
o.rmempty = true;
o.datatype = 'host(0)';
- o = ss.taboption('encryption', form.Value, 'auth_port', _('Radius-Authentication-Port'), _('Default %d').format(1812));
+ o = ss.taboption('encryption', form.Value, 'auth_port', _('RADIUS Authentication Port'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
o.rmempty = true;
o.datatype = 'port';
+ o.placeholder = '1812';
- o = ss.taboption('encryption', form.Value, 'auth_secret', _('Radius-Authentication-Secret'));
+ o = ss.taboption('encryption', form.Value, 'auth_secret', _('RADIUS Authentication Secret'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
o.rmempty = true;
o.password = true;
- o = ss.taboption('encryption', form.Value, 'acct_server', _('Radius-Accounting-Server'));
+ o = ss.taboption('encryption', form.Value, 'acct_server', _('RADIUS Accounting Server'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
o.rmempty = true;
o.datatype = 'host(0)';
- o = ss.taboption('encryption', form.Value, 'acct_port', _('Radius-Accounting-Port'), _('Default %d').format(1813));
+ o = ss.taboption('encryption', form.Value, 'acct_port', _('RADIUS Accounting Port'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
o.rmempty = true;
o.datatype = 'port';
+ o.placeholder = '1813';
- o = ss.taboption('encryption', form.Value, 'acct_secret', _('Radius-Accounting-Secret'));
+ o = ss.taboption('encryption', form.Value, 'acct_secret', _('RADIUS Accounting Secret'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
o.rmempty = true;
o.password = true;
- o = ss.taboption('encryption', form.Value, 'dae_client', _('DAE-Client'));
+ /* extra RADIUS settings start */
+ o = ss.taboption('encryption', form.ListValue, 'dynamic_vlan', _('RADIUS Dynamic VLAN Assignment'), _('Required: Rejects auth if RADIUS server does not provide appropriate VLAN attributes.'));
+ add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
+ o.value('0', _('Disabled'));
+ o.value('1', _('Optional'));
+ o.value('2', _('Required'));
+ o.write = function (section_id, value) {
+ return this.super('write', [section_id, (value == 0) ? null: value]);
+ }
+
+ o = ss.taboption('encryption', form.Flag, 'per_sta_vif', _('RADIUS Per STA VLAN'), _('Each STA is assigned its own AP_VLAN interface.'));
+ add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
+
+ //hostapd internally defaults to vlan_naming=1 even with dynamic VLAN off
+ o = ss.taboption('encryption', form.Flag, 'vlan_naming', _('RADIUS VLAN Naming'), _('Off: <code>vlanXXX</code>, e.g., <code>vlan1</code>. On: <code>vlan_tagged_interface.XXX</code>, e.g. <code>eth0.1</code>.'));
+ add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
+
+ o = ss.taboption('encryption', widgets.DeviceSelect, 'vlan_tagged_interface', _('RADIUS VLAN Tagged Interface'), _('E.g. eth0, eth1'));
+ add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
+ o.size = 1;
+ o.rmempty = true;
+ o.multiple = false;
+ o.noaliases = true;
+ o.nocreate = true;
+ o.noinactive = true;
+
+ o = ss.taboption('encryption', form.Value, 'vlan_bridge', _('RADIUS VLAN Bridge Naming Scheme'), _('E.g. <code>br-vlan</code> or <code>brvlan</code>.'));
+ add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
+ o.rmempty = true;
+ /* extra RADIUS settings end */
+
+ o = ss.taboption('encryption', form.Value, 'dae_client', _('DAE-Client'), _('Dynamic Authorization Extension client.'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
o.rmempty = true;
o.datatype = 'host(0)';
- o = ss.taboption('encryption', form.Value, 'dae_port', _('DAE-Port'), _('Default %d').format(3799));
+ o = ss.taboption('encryption', form.Value, 'dae_port', _('DAE-Port'), _('Dynamic Authorization Extension port.'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
o.rmempty = true;
o.datatype = 'port';
+ o.placeholder = '3799';
- o = ss.taboption('encryption', form.Value, 'dae_secret', _('DAE-Secret'));
+ o = ss.taboption('encryption', form.Value, 'dae_secret', _('DAE-Secret'), _('Dynamic Authorization Extension secret.'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
o.rmempty = true;
o.password = true;
+ //WPA(1) has only WPA IE. Only >= WPA2 has RSN IE Preauth frames.
+ o = ss.taboption('encryption', form.Flag, 'rsn_preauth', _('RSN Preauth'), _('Robust Security Network (RSN): Allow roaming preauth for WPA2-EAP networks (and advertise it in WLAN beacons). Only works if the specified network interface is a bridge. Shortens the time-critical reassociation process.'));
+ add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa2', 'wpa3', 'wpa3-mixed'] });
+
o = ss.taboption('encryption', form.Value, '_wpa_key', _('Key'));
o.depends('encryption', 'psk');
@@ -1428,66 +1547,117 @@ return view.extend({
// Probe 802.11r support (and EAP support as a proxy for Openwrt)
var has_80211r = L.hasSystemFeature('hostapd', '11r') || L.hasSystemFeature('hostapd', 'eap');
- o = ss.taboption('encryption', form.Flag, 'ieee80211r', _('802.11r Fast Transition'), _('Enables fast roaming among access points that belong to the same Mobility Domain'));
+ o = ss.taboption('roaming', form.Flag, 'ieee80211r', _('802.11r Fast Transition'), _('Enables fast roaming among access points that belong to the same Mobility Domain'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
if (has_80211r)
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk-mixed', 'sae', 'sae-mixed'] });
o.rmempty = true;
- o = ss.taboption('encryption', form.Value, 'nasid', _('NAS ID'), _('Used for two different purposes: RADIUS NAS ID and 802.11r R0KH-ID. Not needed with normal WPA(2)-PSK.'));
+ o = ss.taboption('roaming', form.Value, 'nasid', _('NAS ID'), _('Used for two different purposes: RADIUS NAS ID and 802.11r R0KH-ID. Not needed with normal WPA(2)-PSK.'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['wpa', 'wpa2', 'wpa3', 'wpa3-mixed'] });
o.depends({ ieee80211r: '1' });
o.rmempty = true;
- o = ss.taboption('encryption', form.Value, 'mobility_domain', _('Mobility Domain'), _('4-character hexadecimal ID'));
+ o = ss.taboption('roaming', form.Value, 'mobility_domain', _('Mobility Domain'), _('4-character hexadecimal ID'));
o.depends({ ieee80211r: '1' });
o.placeholder = '4f57';
o.datatype = 'and(hexstring,length(4))';
o.rmempty = true;
- o = ss.taboption('encryption', form.Value, 'reassociation_deadline', _('Reassociation Deadline'), _('time units (TUs / 1.024 ms) [1000-65535]'));
+ o = ss.taboption('roaming', form.Value, 'reassociation_deadline', _('Reassociation Deadline'), _('time units (TUs / 1.024 ms) [1000-65535]'));
o.depends({ ieee80211r: '1' });
o.placeholder = '1000';
o.datatype = 'range(1000,65535)';
o.rmempty = true;
- o = ss.taboption('encryption', form.ListValue, 'ft_over_ds', _('FT protocol'));
+ o = ss.taboption('roaming', form.ListValue, 'ft_over_ds', _('FT protocol'));
o.depends({ ieee80211r: '1' });
- o.value('1', _('FT over DS'));
o.value('0', _('FT over the Air'));
+ o.value('1', _('FT over DS'));
o.rmempty = true;
- o = ss.taboption('encryption', form.Flag, 'ft_psk_generate_local', _('Generate PMK locally'), _('When using a PSK, the PMK can be automatically generated. When enabled, the R0/R1 key options below are not applied. Disable this to use the R0 and R1 key options.'));
+ o = ss.taboption('roaming', form.Flag, 'ft_psk_generate_local', _('Generate PMK locally'), _('When using a PSK, the PMK can be automatically generated. When enabled, the R0/R1 key options below are not applied. Disable this to use the R0 and R1 key options.'));
o.depends({ ieee80211r: '1' });
o.default = o.enabled;
o.rmempty = false;
- o = ss.taboption('encryption', form.Value, 'r0_key_lifetime', _('R0 Key Lifetime'), _('minutes'));
+ o = ss.taboption('roaming', form.Value, 'r0_key_lifetime', _('R0 Key Lifetime'), _('minutes'));
o.depends({ ieee80211r: '1' });
o.placeholder = '10000';
o.datatype = 'uinteger';
o.rmempty = true;
- o = ss.taboption('encryption', form.Value, 'r1_key_holder', _('R1 Key Holder'), _('6-octet identifier as a hex string - no colons'));
+ o = ss.taboption('roaming', form.Value, 'r1_key_holder', _('R1 Key Holder'), _('6-octet identifier as a hex string - no colons'));
o.depends({ ieee80211r: '1' });
o.placeholder = '00004f577274';
o.datatype = 'and(hexstring,length(12))';
o.rmempty = true;
- o = ss.taboption('encryption', form.Flag, 'pmk_r1_push', _('PMK R1 Push'));
+ o = ss.taboption('roaming', form.Flag, 'pmk_r1_push', _('PMK R1 Push'));
o.depends({ ieee80211r: '1' });
o.placeholder = '0';
o.rmempty = true;
- o = ss.taboption('encryption', form.DynamicList, 'r0kh', _('External R0 Key Holder List'), _('List of R0KHs in the same Mobility Domain. <br />Format: MAC-address,NAS-Identifier,128-bit key as hex string. <br />This list is used to map R0KH-ID (NAS Identifier) to a destination MAC address when requesting PMK-R1 key from the R0KH that the STA used during the Initial Mobility Domain Association.'));
+ o = ss.taboption('roaming', form.DynamicList, 'r0kh', _('External R0 Key Holder List'), _('List of R0KHs in the same Mobility Domain. <br />Format: MAC-address,NAS-Identifier,256-bit key as hex string. <br />This list is used to map R0KH-ID (NAS Identifier) to a destination MAC address when requesting PMK-R1 key from the R0KH that the STA used during the Initial Mobility Domain Association.'));
o.depends({ ieee80211r: '1' });
o.rmempty = true;
- o = ss.taboption('encryption', form.DynamicList, 'r1kh', _('External R1 Key Holder List'), _ ('List of R1KHs in the same Mobility Domain. <br />Format: MAC-address,R1KH-ID as 6 octets with colons,128-bit key as hex string. <br />This list is used to map R1KH-ID to a destination MAC address when sending PMK-R1 key from the R0KH. This is also the list of authorized R1KHs in the MD that can request PMK-R1 keys.'));
+ o = ss.taboption('roaming', form.DynamicList, 'r1kh', _('External R1 Key Holder List'), _ ('List of R1KHs in the same Mobility Domain. <br />Format: MAC-address,R1KH-ID as 6 octets with colons,256-bit key as hex string. <br />This list is used to map R1KH-ID to a destination MAC address when sending PMK-R1 key from the R0KH. This is also the list of authorized R1KHs in the MD that can request PMK-R1 keys.'));
o.depends({ ieee80211r: '1' });
o.rmempty = true;
// End of 802.11r options
+ // Probe 802.11k and 802.11v support via EAP support (full hostapd has EAP)
+ if (L.hasSystemFeature('hostapd', 'eap')) {
+ /* 802.11k settings start */ o =
+ ss.taboption('roaming', form.Flag, 'ieee80211k', _('802.11k RRM'), _('Radio Resource Measurement - Sends beacons to assist roaming. Not all clients support this.'));
+ // add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk', 'psk2', 'psk-mixed', 'sae', 'sae-mixed'] });
+ o.depends('mode', 'ap');
+ o.depends('mode', 'ap-wds');
+
+ o = ss.taboption('roaming', form.Flag, 'rrm_neighbor_report', _('Neighbour Report'), _('802.11k: Enable neighbor report via radio measurements.'));
+ o.depends({ ieee80211k: '1' });
+ o.default = o.enabled;
+
+ o = ss.taboption('roaming', form.Flag, 'rrm_beacon_report', _('Beacon Report'), _('802.11k: Enable beacon report via radio measurements.'));
+ o.depends({ ieee80211k: '1' });
+ o.default = o.enabled;
+ /* 802.11k settings end */
+
+ /* 802.11v settings start */
+ o = ss.taboption('roaming', form.ListValue, 'time_advertisement', _('Time advertisement'), _('802.11v: Time Advertisement in management frames.'));
+ o.value('0', _('Disabled'));
+ o.value('2', _('Enabled'));
+ o.write = function (section_id, value) {
+ return this.super('write', [section_id, (value == 2) ? value: null]);
+ }
+
+ //Pull current System TZ setting
+ var tz = uci.get('system', '@system[0]', 'timezone');
+ o = ss.taboption('roaming', form.Value, 'time_zone', _('Time zone'), _('802.11v: Local Time Zone Advertisement in management frames.'));
+ o.value(tz);
+ o.rmempty = true;
+
+ o = ss.taboption('roaming', form.Flag, 'wnm_sleep_mode', _('WNM Sleep Mode'), _('802.11v: Wireless Network Management (WNM) Sleep Mode (extended sleep mode for stations).'));
+ o.rmempty = true;
+
+ /* wnm_sleep_mode_no_keys: https://git.openwrt.org/?p=openwrt/openwrt.git;a=commitdiff;h=bf98faaac8ed24cf7d3d93dd4fcd7304d109363b */
+ o = ss.taboption('roaming', form.Flag, 'wnm_sleep_mode_no_keys', _('WNM Sleep Mode Fixes'), _('802.11v: Wireless Network Management (WNM) Sleep Mode Fixes: Prevents reinstallation attacks.'));
+ o.rmempty = true;
+
+ o = ss.taboption('roaming', form.Flag, 'bss_transition', _('BSS Transition'), _('802.11v: Basic Service Set (BSS) transition management.'));
+ o.rmempty = true;
+
+ /* in master, but not 21.02.1: proxy_arp */
+ o = ss.taboption('roaming', form.Flag, 'proxy_arp', _('ProxyARP'), _('802.11v: Proxy ARP enables non-AP STA to remain in power-save for longer.'));
+ o.rmempty = true;
+
+ /* TODO: na_mcast_to_ucast is missing: needs adding to hostapd.sh - nice to have */
+ }
+ /* 802.11v settings end */
+ }
+
+ if (hwtype == 'mac80211') {
o = ss.taboption('encryption', form.ListValue, 'eap_type', _('EAP-Method'));
o.value('tls', 'TLS');
o.value('ttls', 'TTLS');
@@ -1603,38 +1773,43 @@ return view.extend({
if (hwtype == 'mac80211') {
// ieee802.11w options
- if (L.hasSystemFeature('hostapd', '11w')) {
- o = ss.taboption('encryption', form.ListValue, 'ieee80211w', _('802.11w Management Frame Protection'), _("Requires the 'full' version of wpad/hostapd and support from the wifi driver <br />(as of Jan 2019: ath9k, ath10k, mwlwifi and mt76)"));
- o.value('', _('Disabled'));
- o.value('1', _('Optional'));
- o.value('2', _('Required'));
- add_dependency_permutations(o, { mode: ['ap', 'ap-wds', 'sta', 'sta-wds'], encryption: ['owe', 'psk2', 'psk-mixed', 'sae', 'sae-mixed', 'wpa2', 'wpa3', 'wpa3-mixed'] });
-
- o.defaults = {
- '2': [{ encryption: 'sae' }, { encryption: 'owe' }, { encryption: 'wpa3' }, { encryption: 'wpa3-mixed' }],
- '1': [{ encryption: 'sae-mixed'}],
- '': []
- };
-
- o = ss.taboption('encryption', form.Value, 'ieee80211w_max_timeout', _('802.11w maximum timeout'), _('802.11w Association SA Query maximum timeout'));
- o.depends('ieee80211w', '1');
- o.depends('ieee80211w', '2');
- o.datatype = 'uinteger';
- o.placeholder = '1000';
- o.rmempty = true;
-
- o = ss.taboption('encryption', form.Value, 'ieee80211w_retry_timeout', _('802.11w retry timeout'), _('802.11w Association SA Query retry timeout'));
- o.depends('ieee80211w', '1');
- o.depends('ieee80211w', '2');
- o.datatype = 'uinteger';
- o.placeholder = '201';
- o.rmempty = true;
+ o = ss.taboption('encryption', form.ListValue, 'ieee80211w', _('802.11w Management Frame Protection'), _("Note: Some wireless drivers do not fully support 802.11w. E.g. mwlwifi may have problems"));
+ o.value('0', _('Disabled'));
+ o.value('1', _('Optional'));
+ o.value('2', _('Required'));
+ add_dependency_permutations(o, { mode: ['ap', 'ap-wds', 'sta', 'sta-wds'], encryption: ['owe', 'psk2', 'psk-mixed', 'sae', 'sae-mixed', 'wpa2', 'wpa3', 'wpa3-mixed'] });
+
+ o.defaults = {
+ '2': [{ encryption: 'sae' }, { encryption: 'owe' }, { encryption: 'wpa3' }, { encryption: 'wpa3-mixed' }],
+ '1': [{ encryption: 'sae-mixed'}],
+ '0': []
};
+ o.write = function(section_id, value) {
+ if (value != this.default)
+ return form.ListValue.prototype.write.call(this, section_id, value);
+ else
+ return form.ListValue.prototype.remove.call(this, section_id);
+ };
+
+ o = ss.taboption('encryption', form.Value, 'ieee80211w_max_timeout', _('802.11w maximum timeout'), _('802.11w Association SA Query maximum timeout'));
+ o.depends('ieee80211w', '1');
+ o.depends('ieee80211w', '2');
+ o.datatype = 'uinteger';
+ o.placeholder = '1000';
+ o.rmempty = true;
+
+ o = ss.taboption('encryption', form.Value, 'ieee80211w_retry_timeout', _('802.11w retry timeout'), _('802.11w Association SA Query retry timeout'));
+ o.depends('ieee80211w', '1');
+ o.depends('ieee80211w', '2');
+ o.datatype = 'uinteger';
+ o.placeholder = '201';
+ o.rmempty = true;
+
o = ss.taboption('encryption', form.Flag, 'wpa_disable_eapol_key_retries', _('Enable key reinstallation (KRACK) countermeasures'), _('Complicates key reinstallation attacks on the client side by disabling retransmission of EAPOL-Key frames that are used to install keys. This workaround might cause interoperability issues and reduced robustness of key negotiation especially in environments with heavy traffic load.'));
add_dependency_permutations(o, { mode: ['ap', 'ap-wds'], encryption: ['psk2', 'psk-mixed', 'sae', 'sae-mixed', 'wpa2', 'wpa3', 'wpa3-mixed'] });
- if (L.hasSystemFeature('hostapd', 'cli') && L.hasSystemFeature('wpasupplicant')) {
+ if (L.hasSystemFeature('hostapd', 'wps') && L.hasSystemFeature('wpasupplicant')) {
o = ss.taboption('encryption', form.Flag, 'wps_pushbutton', _('Enable WPS pushbutton, requires WPA(2)-PSK/WPA3-SAE'))
o.enabled = '1';
o.disabled = '0';
@@ -1656,15 +1831,15 @@ return view.extend({
};
s.handleScan = function(radioDev, ev) {
- var table = E('div', { 'class': 'table' }, [
- E('div', { 'class': 'tr table-titles' }, [
- E('div', { 'class': 'th col-2 middle center' }, _('Signal')),
- E('div', { 'class': 'th col-4 middle left' }, _('SSID')),
- E('div', { 'class': 'th col-2 middle center hide-xs' }, _('Channel')),
- E('div', { 'class': 'th col-2 middle left hide-xs' }, _('Mode')),
- E('div', { 'class': 'th col-3 middle left hide-xs' }, _('BSSID')),
- E('div', { 'class': 'th col-3 middle left' }, _('Encryption')),
- E('div', { 'class': 'th cbi-section-actions right' }, ' '),
+ var table = E('table', { 'class': 'table' }, [
+ E('tr', { 'class': 'tr table-titles' }, [
+ E('th', { 'class': 'th col-2 middle center' }, _('Signal')),
+ E('th', { 'class': 'th col-4 middle left' }, _('SSID')),
+ E('th', { 'class': 'th col-2 middle center hide-xs' }, _('Channel')),
+ E('th', { 'class': 'th col-2 middle left hide-xs' }, _('Mode')),
+ E('th', { 'class': 'th col-3 middle left hide-xs' }, _('BSSID')),
+ E('th', { 'class': 'th col-3 middle left' }, _('Encryption')),
+ E('th', { 'class': 'th cbi-section-actions right' }, ' '),
])
]);
@@ -1742,7 +1917,7 @@ return view.extend({
E('span', { 'style': s }, '%h'.format(network.formatWifiEncryption(res.encryption))),
E('div', { 'class': 'right' }, E('button', {
'class': 'cbi-button cbi-button-action important',
- 'click': L.bind(this.handleJoin, this, radioDev, res)
+ 'click': ui.createHandlerFn(this, 'handleJoin', radioDev, res)
}, _('Join Network')))
]);
@@ -1790,17 +1965,19 @@ return view.extend({
s.handleJoinConfirm = function(radioDev, bss, form, ev) {
var nameopt = L.toArray(form.lookupOption('name', '_new_'))[0],
passopt = L.toArray(form.lookupOption('password', '_new_'))[0],
+ ssidopt = L.toArray(form.lookupOption('ssid', '_new_'))[0],
bssidopt = L.toArray(form.lookupOption('bssid', '_new_'))[0],
zoneopt = L.toArray(form.lookupOption('zone', '_new_'))[0],
replopt = L.toArray(form.lookupOption('replace', '_new_'))[0],
nameval = (nameopt && nameopt.isValid('_new_')) ? nameopt.formvalue('_new_') : null,
passval = (passopt && passopt.isValid('_new_')) ? passopt.formvalue('_new_') : null,
+ ssidval = (ssidopt && ssidopt.isValid('_new_')) ? ssidopt.formvalue('_new_') : null,
bssidval = (bssidopt && bssidopt.isValid('_new_')) ? bssidopt.formvalue('_new_') : null,
zoneval = zoneopt ? zoneopt.formvalue('_new_') : null,
enc = L.isObject(bss.encryption) ? bss.encryption : null,
is_wep = (enc && Array.isArray(enc.wep)),
- is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' })),
- is_sae = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'sae' }));
+ is_psk = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'psk' }).length > 0),
+ is_sae = (enc && Array.isArray(enc.wpa) && L.toArray(enc.authentication).filter(function(a) { return a == 'sae' }).length > 0);
if (nameval == null || (passopt && passval == null))
return;
@@ -1841,6 +2018,9 @@ return view.extend({
uci.set('wireless', section_id, 'bssid', bss.bssid);
}
+ if (ssidval != null)
+ uci.set('wireless', section_id, 'ssid', ssidval);
+
if (is_sae) {
uci.set('wireless', section_id, 'encryption', 'sae');
uci.set('wireless', section_id, 'key', passval);
@@ -1881,12 +2061,14 @@ return view.extend({
});
});
}).then(L.bind(function() {
+ ui.showModal(null, E('p', { 'class': 'spinning' }, [ _('Loading data…') ]));
+
return this.renderMoreOptionsModal(section_id);
}, this));
};
s.handleJoin = function(radioDev, bss, ev) {
- this.handleScanAbort(ev);
+ poll.remove(this.pollFn);
var m2 = new form.Map('wireless'),
s2 = m2.section(form.NamedSection, '_new_'),
@@ -1911,6 +2093,11 @@ return view.extend({
]).then(this.renderContents.bind(this));
};
+ if (bss.ssid == null) {
+ name = s2.option(form.Value, 'ssid', _('Network SSID'), _('The correct SSID must be manually specified when joining a hidden wireless network'));
+ name.rmempty = false;
+ };
+
replace = s2.option(form.Flag, 'replace', _('Replace wireless configuration'), _('Check this option to delete the existing networks from this radio.'));
name = s2.option(form.Value, 'name', _('Name of the new network'), _('The allowed characters are: <code>A-Z</code>, <code>a-z</code>, <code>0-9</code> and <code>_</code>'));
@@ -2063,13 +2250,13 @@ return view.extend({
.then(L.bind(this.poll_status, this, nodes));
}, this), 5);
- var table = E('div', { 'class': 'table assoclist', 'id': 'wifi_assoclist_table' }, [
- E('div', { 'class': 'tr table-titles' }, [
- E('div', { 'class': 'th nowrap' }, _('Network')),
- E('div', { 'class': 'th hide-xs' }, _('MAC-Address')),
- E('div', { 'class': 'th' }, _('Host')),
- E('div', { 'class': 'th' }, _('Signal / Noise')),
- E('div', { 'class': 'th' }, _('RX Rate / TX Rate'))
+ var table = E('table', { 'class': 'table assoclist', 'id': 'wifi_assoclist_table' }, [
+ E('tr', { 'class': 'tr table-titles' }, [
+ E('th', { 'class': 'th nowrap' }, _('Network')),
+ E('th', { 'class': 'th hide-xs' }, _('MAC address')),
+ E('th', { 'class': 'th' }, _('Host')),
+ E('th', { 'class': 'th' }, _('Signal / Noise')),
+ E('th', { 'class': 'th' }, _('RX Rate / TX Rate'))
])
]);
@@ -2077,5 +2264,7 @@ return view.extend({
return E([ nodes, E('h3', _('Associated Stations')), table ]);
}, this, m));
- }
+ },
+
+ handleReset: null
});
diff --git a/modules/luci-mod-network/root/usr/share/luci/menu.d/luci-mod-network.json b/modules/luci-mod-network/root/usr/share/luci/menu.d/luci-mod-network.json
index 188c695f3f..2fa3cf6ab3 100644
--- a/modules/luci-mod-network/root/usr/share/luci/menu.d/luci-mod-network.json
+++ b/modules/luci-mod-network/root/usr/share/luci/menu.d/luci-mod-network.json
@@ -46,47 +46,35 @@
}
},
- "admin/network/dhcp": {
- "title": "DHCP and DNS",
+ "admin/network/routes": {
+ "title": "Routing",
"order": 30,
"action": {
"type": "view",
- "path": "network/dhcp"
+ "path": "network/routes"
},
"depends": {
- "acl": [ "luci-mod-network-dhcp" ],
- "uci": { "dhcp": true }
+ "acl": [ "luci-mod-network-config" ]
}
},
- "admin/network/hosts": {
- "title": "Hostnames",
+ "admin/network/dhcp": {
+ "title": "DHCP and DNS",
"order": 40,
"action": {
"type": "view",
- "path": "network/hosts"
+ "path": "network/dhcp"
},
"depends": {
"acl": [ "luci-mod-network-dhcp" ],
+ "fs": { "/usr/sbin/dnsmasq": "executable" },
"uci": { "dhcp": true }
}
},
- "admin/network/routes": {
- "title": "Static Routes",
- "order": 50,
- "action": {
- "type": "view",
- "path": "network/routes"
- },
- "depends": {
- "acl": [ "luci-mod-network-config" ]
- }
- },
-
"admin/network/diagnostics": {
"title": "Diagnostics",
- "order": 60,
+ "order": 50,
"action": {
"type": "view",
"path": "network/diagnostics"
diff --git a/modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json b/modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json
index d6c84bab27..b377f395f0 100644
--- a/modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json
+++ b/modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json
@@ -4,7 +4,13 @@
"read": {
"cgi-io": [ "exec" ],
"file": {
- "/usr/libexec/luci-peeraddr": [ "exec" ]
+ "/etc/iproute2/rt_tables": [ "read" ],
+ "/proc/sys/net/ipv6/conf/*/mtu": [ "read" ],
+ "/proc/sys/net/ipv6/conf/*/hop_limit": [ "read" ],
+ "/usr/libexec/luci-peeraddr": [ "exec" ],
+ "/usr/lib/opkg/info/netifd.control": [ "read" ],
+ "/proc/sys/net/ipv[46]/conf/*": [ "read" ],
+ "/sys/class/net/*/brport/*": [ "read" ]
},
"ubus": {
"file": [ "exec" ],
@@ -54,7 +60,8 @@
"/usr/bin/ping": [ "exec" ],
"/usr/bin/ping6": [ "exec", "list" ],
"/usr/bin/traceroute": [ "exec" ],
- "/usr/bin/traceroute6": [ "exec", "list" ]
+ "/usr/bin/traceroute6": [ "exec", "list" ],
+ "/usr/bin/arp-scan": [ "exec", "list" ]
},
"ubus": {
"file": [ "exec", "stat" ]