luci-mod-network: add support for bridge vlan filtering
authorJo-Philipp Wich <jo@mein.io>
Tue, 28 Jul 2020 18:51:10 +0000 (20:51 +0200)
committerRafał Miłecki <rafal@milecki.pl>
Thu, 27 May 2021 10:19:05 +0000 (12:19 +0200)
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
(cherry picked from commit eeef38d53412a78f185f0bb53153ecb3fe9e8838)

modules/luci-mod-network/htdocs/luci-static/resources/tools/network.js
modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js

index ae506dbec68b39857e730181456d9d31752a61d2..d5eea4c8262d6fe9e254ed62a05462b444699ceb 100644 (file)
@@ -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);
        }
 });
index a93fb4122c0d2ed99b6c6b46593eac751a55f79b..fbc21c538fb4b310b9d7ff520b9f6ba762b47a7f 100644 (file)
@@ -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'));
 
@@ -801,9 +802,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_'),
@@ -907,8 +922,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'))
@@ -1058,6 +1074,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');