summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-mod-network
diff options
context:
space:
mode:
authorJo-Philipp Wich <jo@mein.io>2020-07-28 20:51:10 +0200
committerJo-Philipp Wich <jo@mein.io>2021-03-15 11:40:30 +0100
commiteeef38d53412a78f185f0bb53153ecb3fe9e8838 (patch)
treec01ecb529e1fae17ecd73b249f823b203b695d93 /modules/luci-mod-network
parentfaad7464a8ed88a251c621dc92e8cbfb146bcd7f (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.js331
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js29
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');