diff options
author | Jo-Philipp Wich <jo@mein.io> | 2020-07-28 20:51:10 +0200 |
---|---|---|
committer | Jo-Philipp Wich <jo@mein.io> | 2021-03-15 11:40:30 +0100 |
commit | eeef38d53412a78f185f0bb53153ecb3fe9e8838 (patch) | |
tree | c01ecb529e1fae17ecd73b249f823b203b695d93 /modules/luci-mod-network | |
parent | faad7464a8ed88a251c621dc92e8cbfb146bcd7f (diff) |
luci-mod-network: add support for bridge vlan filtering
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'modules/luci-mod-network')
-rw-r--r-- | modules/luci-mod-network/htdocs/luci-static/resources/tools/network.js | 331 | ||||
-rw-r--r-- | modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js | 29 |
2 files changed, 359 insertions, 1 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 index ae506dbec6..d5eea4c826 100644 --- a/modules/luci-mod-network/htdocs/luci-static/resources/tools/network.js +++ b/modules/luci-mod-network/htdocs/luci-static/resources/tools/network.js @@ -1,4 +1,5 @@ 'use strict'; +'require ui'; 'require uci'; 'require form'; 'require network'; @@ -85,6 +86,18 @@ function isBridgePort(dev) { return isPort; } +function renderDevBadge(dev) { + var type = dev.getType(), up = dev.isUp(); + + return E('span', { 'class': 'ifacebadge', 'style': 'font-weight:normal' }, [ + E('img', { + 'class': 'middle', + 'src': L.resource('icons/%s%s.png').format(type, up ? '' : '_disabled') + }), + ' ', dev.getName() + ]); +} + function lookupDevName(s, section_id) { var typeui = s.getUIElement(section_id, 'type'), typeval = typeui ? typeui.getValue() : s.cfgvalue(section_id, 'type'), @@ -180,6 +193,162 @@ function deviceRefresh(section_id) { } } + +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 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); @@ -649,5 +818,167 @@ return baseclass.extend({ o.default = o.disabled; o.depends(Object.assign({ multicast: '1' }, simpledep)); } + + o = this.addOption(s, 'bridgevlan', form.Flag, 'vlan_filtering', _('Enable VLAN filterering')); + o.depends('type', 'bridge'); + o.updateDefaultValue = function(section_id) { + var device = isIface ? 'br-%s'.format(s.section) : 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'); + o.renderWidget = function(/* ... */) { + return form.SectionValue.prototype.renderWidget.apply(this, arguments).then(L.bind(function(node) { + node.style.overflowX = 'auto'; + node.style.overflowY = 'visible'; + node.style.paddingBottom = '100px'; + node.style.marginBottom = '-100px'; + + return node; + }, this)); + }; + + 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('middle'); + }); + + return node; + }; + + ss.filter = function(section_id) { + var devname = isIface ? 'br-%s'.format(s.section) : 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) { + 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])); + 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 = isIface ? 'br-%s'.format(s.section) : 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); + + 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 = isIface + ? (ifc.getDevices() || L.toArray(ifc.getDevice())).map(function(dev) { return dev.getName() }) + : L.toArray(uci.get('network', s.section, 'ifname')); + + ss.updatePorts(ports); } }); 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 c8be569430..0829a92fdd 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 @@ -333,6 +333,7 @@ return view.extend({ 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')); @@ -717,9 +718,23 @@ 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'), + ifname = (type == 'bridge') ? 'br-%s'.format(this.activeSection || this.addedSection) : null; + + uci.sections('network', 'bridge-vlan', function(bvs) { + if (ifname != null && bvs.device == ifname) + 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_'), @@ -823,8 +838,9 @@ return view.extend({ protoclass.addDevice(dev); }); } - }).then(L.bind(m.children[0].renderMoreOptionsModal, m.children[0], nameval)); + m.children[0].addedSection = section_id; + }).then(L.bind(m.children[0].renderMoreOptionsModal, m.children[0], nameval)); }); }) }, _('Create interface')) @@ -974,6 +990,17 @@ return view.extend({ nettools.addDeviceOptions(s, dev, isNew); }; + s.handleModalCancel = function(/* ... */) { + 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']); + }); + + 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'); |