diff options
Diffstat (limited to 'modules/luci-mod-network')
7 files changed, 2008 insertions, 317 deletions
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..2ebf3afe02 --- /dev/null +++ b/modules/luci-mod-network/htdocs/luci-static/resources/tools/network.js @@ -0,0 +1,999 @@ +'use strict'; +'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 setIfActive(section_id, value) { + if (this.isActive(section_id)) { + uci.set('network', section_id, this.ucioption, value); + + /* Requires http://lists.openwrt.org/pipermail/openwrt-devel/2020-July/030397.html */ + if (false && this.option == 'ifname_multi') { + var devname = this.section.formvalue(section_id, 'name_complex'), + m = devname ? devname.match(/^br-([A-Za-z0-9_]+)$/) : null; + + if (m && uci.get('network', m[1], 'type') == 'bridge') { + uci.set('network', m[1], 'ifname', devname); + uci.unset('network', m[1], 'type'); + } + } + } +} + +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, ignore_type_match) { + var exists = false; + + uci.sections('network', 'device', function(ss) { + exists = exists || ( + ss['.name'] != section_id && + ss.name == devname && + (!ignore_type_match || !ignore_type_match.test(ss.type || '')) + ); + }); + + 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; + + if (carrier && speed > 0 && duplex != null) + desc = E('abbr', { + 'title': '%d MBit/s, %s'.format(speed, duplex == 'full' ? _('full-duplex') : _('half-duplex')) + }, [ '%d%s'.format(speed, duplex == 'full' ? 'FD' : 'HD') ]); + else if (carrier) + desc = document.createTextNode(_('Connected')); + else + desc = document.createTextNode(_('no link')); + + dom.content(node, [ desc ]); + + return node; +} + +function renderPortStatus(dev) { + return updatePortStatus(E('span', { + 'class': 'port-status-link', + 'data-device': dev.getName() + }), dev); +} + +function lookupDevName(s, section_id) { + var typeui = s.getUIElement(section_id, 'type'), + typeval = typeui ? typeui.getValue() : s.cfgvalue(section_id, 'type'), + ifnameui = s.getUIElement(section_id, 'ifname_single'), + ifnameval = ifnameui ? ifnameui.getValue() : s.cfgvalue(section_id, 'ifname_single'); + + return (typeval == 'bridge') ? 'br-%s'.format(section_id) : ifnameval; +} + +function lookupDevSection(s, section_id, autocreate) { + var devname = lookupDevName(s, section_id), + devsection = null; + + uci.sections('network', 'device', function(ds) { + if (ds.name == devname) + devsection = ds['.name']; + }); + + if (autocreate && !devsection) { + devsection = uci.add('network', 'device'); + uci.set('network', devsection, 'name', devname); + } + + return devsection; +} + +function getDeviceValue(dev, method) { + if (dev && dev.getL3Device) + dev = dev.getL3Device(); + + if (dev && typeof(dev[method]) == 'function') + return dev[method].apply(dev); + + return ''; +} + +function deviceCfgValue(section_id) { + if (arguments.length == 2) + return; + + var ds = lookupDevSection(this.section, section_id, false); + + return (ds ? uci.get('network', ds, this.option) : null) || + (this.migrate ? uci.get('network', section_id, this.option) : null) || + this.default; +} + +function deviceWrite(section_id, formvalue) { + var ds = lookupDevSection(this.section, section_id, true); + + uci.set('network', ds, this.option, formvalue); + + if (this.migrate) + uci.unset('network', section_id, this.option); +} + +function deviceRemove(section_id) { + var ds = lookupDevSection(this.section, section_id, false); + + uci.unset('network', ds, this.option); + + if (this.migrate) + uci.unset('network', section_id, this.option); +} + +function deviceRefresh(section_id) { + var dev = network.instantiateDevice(lookupDevName(this.section, section_id)), + uielem = this.getUIElement(section_id); + + if (uielem) { + switch (this.option) { + case 'mtu': + case 'mtu6': + uielem.setPlaceholder(dev.getMTU()); + break; + + case 'macaddr': + uielem.setPlaceholder(dev.getMAC()); + break; + } + + uielem.setValue(this.cfgvalue(section_id)); + } +} + +function sectionParse() { + var ds = lookupDevSection(this, this.section, false); + + return form.NamedSection.prototype.parse.apply(this).then(function() { + var sv = ds ? uci.get('network', ds) : null; + + if (sv) { + var empty = true; + + for (var opt in sv) { + if (opt.charAt(0) == '.' || opt == 'name') + continue; + + empty = false; + } + + if (empty) + uci.remove('network', ds); + } + }); +} + + +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' }, [ _('Do not participate', 'VLAN port state') ]) + ]), + 'u': E([], [ + E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 'u' ]), + E('span', { 'class': 'hide-close' }, [ _('Egress untagged', 'VLAN port state') ]) + ]), + 't': E([], [ + E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 't' ]), + E('span', { 'class': 'hide-close' }, [ _('Egress tagged', 'VLAN port state') ]) + ]), + '*': E([], [ + E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '*' ]), + E('span', { 'class': 'hide-close' }, [ _('Primary VLAN ID', '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 pname = this.port, + spec = L.toArray(uci.get('network', section_id, 'ports')).filter(function(p) { return p.replace(/:[ut*]+$/, '') == pname })[0]; + + if (spec && spec.match(/t/)) + return spec.match(/\*/) ? ['t', '*'] : ['t']; + else if (spec) + return spec.match(/\*/) ? ['u', '*'] : ['u']; + else + 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); + }, + + 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); + }, + + addOption: function(s, tabName, optionClass, optionName, optionTitle, optionDescription) { + var o = this.replaceOption(s, tabName, optionClass, optionName, optionTitle, optionDescription); + + if (s.sectiontype == 'interface' && optionName != 'type' && optionName != 'vlan_filtering') { + o.migrate = true; + o.cfgvalue = deviceCfgValue; + o.write = deviceWrite; + o.remove = deviceRemove; + o.refresh = deviceRefresh; + } + + return o; + }, + + addDeviceOptions: function(s, dev, isNew) { + var 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.addOption(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 = this.addOption(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.write = o.remove = setIfActive; + o.filter = function(section_id, value) { + return !deviceSectionExists(section_id, value, /^(?:bridge|8021q|8021ad|macvlan|veth)$/); + }; + o.validate = function(section_id, value) { + return deviceSectionExists(section_id, value, /^(?:bridge|8021q|8021ad|macvlan|veth)$/) + ? _('A configuration for the device "%s" already exists').format(value) : true; + }; + o.depends('type', ''); + + o = this.addOption(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.validate = function(section_id, value) { + 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.write = o.remove = setIfActive; + o.depends('type', '8021q'); + o.depends('type', '8021ad'); + o.depends('type', 'macvlan'); + + o = this.addOption(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()) { + name.setValue('%s.%d'.format(base, vid)); + name.triggerValidation(); + } + + return true; + }; + o.depends('type', '8021q'); + o.depends('type', '8021ad'); + + o = this.addOption(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.addOption(s, 'devgeneral', form.Value, 'name_complex', _('Device name')); + o.rmempty = false; + o.datatype = 'maxlength(15)'; + o.readonly = !isNew; + o.ucioption = 'name'; + o.write = o.remove = setIfActive; + o.validate = function(section_id, value) { + return deviceSectionExists(section_id, value, /^$/) ? _('The device name "%s" is already taken').format(value) : true; + }; + o.depends({ type: '', '!reverse': true }); + + o = this.addOption(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.addOption(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.addOption(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.write = o.remove = setIfActive; + 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.addOption(s, 'devgeneral', form.Value, 'mtu', _('MTU')); + o.placeholder = getDeviceValue(dev, 'getMTU'); + o.datatype = 'max(9200)'; + o.depends('type', ''); + o.depends('type', 'bridge'); + + o = this.addOption(s, 'devgeneral', form.Value, 'macaddr', _('MAC address')); + o.placeholder = getDeviceValue(dev, 'getMAC'); + o.datatype = 'macaddr'; + o.depends('type', ''); + o.depends('type', 'bridge'); + o.depends('type', 'macvlan'); + o.depends('type', 'veth'); + + o = this.addOption(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.addOption(s, 'devgeneral', form.Value, 'peer_macaddr', _('Peer MAC address')); + o.rmempty = true; + o.datatype = 'macaddr'; + o.depends('type', 'veth'); + + o = this.addOption(s, 'devgeneral', form.Value, 'txqueuelen', _('TX queue length')); + o.placeholder = dev ? dev._devstate('qlen') : ''; + o.datatype = 'uinteger'; + o.depends('type', ''); + + o = this.addOption(s, 'devadvanced', form.Flag, 'promisc', _('Enable promiscuous mode')); + o.default = o.disabled; + o.depends('type', ''); + + o = this.addOption(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(section_id) { + var val = form.ListValue.prototype.cfgvalue.apply(this, [section_id]); + + switch (val || '') { + case 'loose': + case '1': + return 'loose'; + + case 'strict': + case '2': + return 'strict'; + + default: + return ''; + } + }; + o.depends('type', ''); + + o = this.addOption(s, 'devadvanced', form.Flag, 'acceptlocal', _('Accept local'), _('Accept packets with local source addresses')); + o.default = o.disabled; + o.depends('type', ''); + + o = this.addOption(s, 'devadvanced', form.Flag, 'sendredirects', _('Send ICMP redirects')); + o.default = o.enabled; + o.depends('type', ''); + + o = this.addOption(s, 'devadvanced', form.Value, 'neighreachabletime', _('Neighbour cache validity'), _('Time in milliseconds')); + o.placeholder = '30000'; + o.datatype = 'uinteger'; + o.depends('type', ''); + + o = this.addOption(s, 'devadvanced', form.Value, 'neighgcstaletime', _('Stale neighbour cache timeout'), _('Timeout in seconds')); + o.placeholder = '60'; + o.datatype = 'uinteger'; + o.depends('type', ''); + + o = this.addOption(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.depends('type', ''); + + o = this.addOption(s, 'devgeneral', form.Flag, 'ipv6', _('Enable IPv6')); + o.migrate = false; + o.default = o.enabled; + o.depends('type', ''); + + o = this.addOption(s, 'devgeneral', form.Value, 'mtu6', _('IPv6 MTU')); + o.placeholder = getDeviceValue(dev, 'getMTU'); + o.datatype = 'max(9200)'; + o.depends(Object.assign({ ipv6: '1' }, 'type', '')); + + o = this.addOption(s, 'devgeneral', form.Value, 'dadtransmits', _('DAD transmits'), _('Amount of Duplicate Address Detection probes to send')); + o.placeholder = '1'; + o.datatype = 'uinteger'; + o.depends(Object.assign({ ipv6: '1' }, 'type', '')); + + + o = this.addOption(s, 'devadvanced', form.Flag, 'multicast', _('Enable multicast support')); + o.default = o.enabled; + o.depends('type', ''); + + o = this.addOption(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(Object.assign({ multicast: '1' }, 'type', '')); + + o = this.addOption(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(Object.assign({ multicast: '1' }, 'type', '')); + + if (isBridgePort(dev)) { + o = this.addOption(s, 'brport', form.Flag, 'learning', _('Enable MAC address learning')); + o.default = o.enabled; + o.depends('type', ''); + + o = this.addOption(s, 'brport', form.Flag, 'unicast_flood', _('Enable unicast flooding')); + o.default = o.enabled; + o.depends('type', ''); + + o = this.addOption(s, 'brport', form.Flag, 'isolated', _('Port isolation'), _('Only allow communication with non-isolated bridge ports when enabled')); + o.default = o.disabled; + o.depends('type', ''); + + o = this.addOption(s, 'brport', form.ListValue, 'multicast_router', _('Multicast routing')); + o.value('', _('Never')); + o.value('1', _('Learn')); + o.value('2', _('Always')); + o.depends(Object.assign({ multicast: '1' }, 'type', '')); + + o = this.addOption(s, 'brport', form.Flag, 'multicast_to_unicast', _('Multicast to unicast'), _('Forward multicast packets as unicast packets on this device.')); + o.default = o.disabled; + o.depends(Object.assign({ multicast: '1' }, 'type', '')); + + o = this.addOption(s, 'brport', form.Flag, 'multicast_fast_leave', _('Enable multicast fast leave')); + o.default = o.disabled; + o.depends(Object.assign({ multicast: '1' }, 'type', '')); + } + + o = this.addOption(s, 'bridgevlan', form.Flag, 'vlan_filtering', _('Enable VLAN filterering')); + 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.addOption(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(); + }); + + 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) { + 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); + + ports.sort(function(a, b) { + var m1 = a.match(/^(.+?)([0-9]*)$/), + m2 = b.match(/^(.+?)([0-9]*)$/); + + if (m1[1] < m2[1]) + return -1; + else if (m1[1] > m2[1]) + return 1; + else + return +(m1[2] || 0) - +(m2[2] || 0); + }); + + 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..4dd90cc326 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 @@ -31,15 +31,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,15 +50,15 @@ 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...'))) ]) ]) ]); @@ -282,6 +282,13 @@ return view.extend({ o.validate = validateServerSpec; + o = s.taboption('general', form.DynamicList, 'address', _('Addresses'), + _('List of domains to force to an IP address.')); + + o.optional = true; + o.placeholder = '/router.local/192.168.0.1'; + + o = s.taboption('general', form.Flag, 'rebind_protection', _('Rebind protection'), _('Discard upstream RFC1918 responses')); @@ -402,7 +409,7 @@ return view.extend({ 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.')); + _('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.')); ss = o.subsection; @@ -425,12 +432,9 @@ return view.extend({ so.datatype = 'list(unique(macaddr))'; 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( @@ -446,7 +450,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,16 +463,35 @@ 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; }; 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.write = function(section, value) { + var ip = this.map.lookupOption('ip', section)[0].formvalue(section); + var hosts = uci.sections('dhcp', 'host'); + var section_removed = false; + + for (var i = 0; i < hosts.length; i++) { + if (ip == hosts[i].ip) { + uci.set('dhcp', hosts[i]['.name'], 'mac', [hosts[i].mac, value].join(' ')); + uci.remove('dhcp', section); + section_removed = true; + break; + } + } + + if (!section_removed) { + uci.set('dhcp', section, 'mac', value); + } + } + so = ss.option(form.Value, 'ip', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Address')); so.datatype = 'or(ip4addr,"ignore")'; so.validate = function(section, value) { @@ -478,11 +505,18 @@ return view.extend({ return true; }; + + var ipaddrs = {}; + 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); - } + var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4); + + for (var i = 0; i < addrs.length; i++) + ipaddrs[addrs[i]] = hosts[mac].name; + }); + + 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')); @@ -540,7 +574,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..5d6bd4765e 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 @@ -66,9 +66,9 @@ return view.extend({ return E([], [ E('h2', {}, [ _('Network Utilities') ]), - E('div', { 'class': 'table' }, [ - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left' }, [ + E('table', { 'class': 'table' }, [ + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td left' }, [ E('input', { 'style': 'margin:5px 0', 'type': 'text', @@ -91,7 +91,7 @@ return view.extend({ ]) ]), - E('div', { 'class': 'td left' }, [ + E('td', { 'class': 'td left' }, [ E('input', { 'style': 'margin:5px 0', 'type': 'text', @@ -114,7 +114,7 @@ return view.extend({ ]) ]), - E('div', { 'class': 'td left' }, [ + E('td', { 'class': 'td left' }, [ E('input', { 'style': 'margin:5px 0', 'type': 'text', 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 index cd0dacbf67..93ebf5ba68 100644 --- 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 @@ -31,11 +31,18 @@ return view.extend({ 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 - )); + + 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) { + o.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4])); }); 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..a8fa727da7 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,54 @@ 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; } +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 +319,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 +477,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')); @@ -370,7 +526,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; @@ -394,6 +550,7 @@ return view.extend({ }, this); o.write = function() {}; + proto_select = s.taboption('general', form.ListValue, 'proto', _('Protocol')); proto_select.modalonly = true; @@ -409,84 +566,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 +619,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 +632,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()); @@ -567,9 +648,15 @@ return view.extend({ 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')) @@ -578,107 +665,378 @@ return view.extend({ 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.'), + 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); + + if (checked == '1' || protoval != 'static') { + 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 (protoval == 'static') { + 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) { + uci.set('dhcp', section_id, 'ra_flags', [ 'none' ]); + }; + + 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 = 'or(uinteger,ip4addr("nomask"))'; - so.default = '100'; + so.datatype = 'uinteger'; + so.placeholder = '600'; + 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_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.datatype = 'uinteger'; - so.default = '150'; + so.placeholder = '200'; + 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_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.default = '12h'; + so.datatype = 'range(0, 9000)'; + so.placeholder = '1800'; + 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_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(); - ss.taboption('advanced', form.Flag, 'force', _('Force'), _('Force DHCP on this network even if another server is detected.')); + if (dev) { + var path = "/proc/sys/net/ipv6/conf/%s/mtu".format(dev.getName()); - // XXX: is this actually useful? - //ss.taboption('advanced', form.Value, 'name', _('Name'), _('Define a name for this network.')); + return L.resolveDefault(fs.read(path), dev.getMTU()).then(L.bind(function(data) { + this.placeholder = data; + }, this)); + } + }; - 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 = 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(); - 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 ]); - }; + if (dev) { + var path = "/proc/sys/net/ipv6/conf/%s/hop_limit".format(dev.getName()); - 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 L.resolveDefault(fs.read(path), 64).then(L.bind(function(data) { + this.placeholder = data; + }, 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.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('dhcpv6', 'server'); - so.depends('dhcpv6', 'hybrid'); - so.default = '1'; + so.depends({ dhcpv6: 'hybrid', master: '0' }); - so = ss.taboption('ipv6', form.Flag, 'ra_default', _('Always announce default router'), _('Announce as default router even if no public prefix is available.')); - so.depends('ra', 'server'); - so.depends('ra', 'hybrid'); + 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({ 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('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'), ' '); + + + so = ss.taboption('ipv6', form.Flag, 'ndproxy_routing', _('Learn routes'), _('Setup routes for proxied IPv6 neighbours.')); + so.default = so.enabled; + so.depends('ndp', 'relay'); + so.depends('ndp', 'hybrid'); - 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_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' }); } 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 (protoval != 'static') { + 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 (protoval != 'static') + 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][0], rtTables[i][1])); + + 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,14 +1045,28 @@ 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(); @@ -727,30 +1099,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 +1123,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 +1139,11 @@ 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(' ')); - } + m.children[0].addedSection = section_id; }).then(L.bind(m.children[0].renderMoreOptionsModal, m.children[0], nameval)); - }); }) }, _('Create interface')) @@ -863,20 +1214,236 @@ 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 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:(.+)$/); - this.default = (protoval == 'static') ? this.enabled : this.disabled; - return this.super('render', [ option_index, section_id, in_table ]); + if (m) { + var devtype = getDevType(section_id); + + /* Treat not explicitly configured, preexisting VLAN interfaces + as simple network devices when adding configuration for them, + since it is more likely that people want to set general device + properties such as MAC address instead of reconfiguring ingress/ + egress QoS mapping, which is the only editable property of + preexisting VLAN device config dialogs. + + Ref: https://github.com/openwrt/luci/issues/5102 + */ + if (devtype == '8021q') + devtype = 'ethernet'; + + section_id = uci.add('network', 'device'); + + 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 = _('Reset'); + 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]); + + return form.GridSection.prototype.handleModalCancel.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. Original: %s').format(mac || _('unknown')) + }, [ 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. Original: %s').format(mtu || _('unknown')) + }, [ 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 ‘local’ 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.')); 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..5115a69eb6 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,16 +312,32 @@ 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': [], + '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++) { + var band; + + if (data[1][i].mhz >= 2412 && data[1][i].mhz <= 2484) + band = '2g'; + else if (data[1][i].mhz >= 5160 && data[1][i].mhz <= 5885) + band = '5g'; + else if (data[1][i].mhz >= 5925 && data[1][i].mhz <= 7125) + band = '6g'; + else if (data[1][i].mhz >= 58329 && data[1][i].mhz <= 69120) + band = '60g'; + else + continue; + + 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 }, {}); @@ -320,7 +345,8 @@ var CBIWifiFrequencyValue = form.Value.extend({ this.modes = [ '', 'Legacy', true, 'n', 'N', hwmodelist.n, - 'ac', 'AC', hwmodelist.ac + 'ac', 'AC', hwmodelist.ac, + 'ax', 'AX', hwmodelist.ax ]; var htmodelist = L.toArray(data[0] ? data[0].getHTModes() : null) @@ -337,20 +363,30 @@ 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 ], '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 ] }; }, this)); @@ -392,6 +428,8 @@ var CBIWifiFrequencyValue = form.Value.extend({ this.setValues(band, this.bands[mode.value]); this.toggleWifiChannel(elem); + + this.map.checkDepends(); }, toggleWifiChannel: function(elem) { @@ -408,11 +446,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 +462,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].value; return elem; }, @@ -461,6 +511,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 +520,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 +533,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 +550,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 +706,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', @@ -850,7 +907,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 +940,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': '11g', '!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")'; @@ -977,8 +1040,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 +1080,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')); @@ -1063,11 +1135,11 @@ 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; @@ -1603,38 +1675,36 @@ 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('', _('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.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 +1726,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 +1812,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 +1860,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 +1913,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); @@ -1886,7 +1961,7 @@ return view.extend({ }; 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 +1986,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 +2143,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')) ]) ]); 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..6943d95637 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,11 @@ "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" ] }, "ubus": { "file": [ "exec" ], |