Browse Source

Merge pull request #4307 from jow-/uci-network-device-support

Introduce support for managing `config device` and `config bridge-vlan` sections
Jo-Philipp Wich 3 years ago
parent
commit
4cde2bda73
28 changed files with 1336 additions and 384 deletions
  1. 2 7
      applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/rules.js
  2. 2 3
      applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/snats.js
  3. 0 15
      modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js
  4. 0 30
      modules/luci-base/htdocs/luci-static/resources/protocol/static.js
  5. 984 0
      modules/luci-mod-network/htdocs/luci-static/resources/tools/network.js
  6. 346 106
      modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js
  7. 1 0
      modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json
  8. 0 15
      protocols/luci-proto-3g/htdocs/luci-static/resources/protocol/3g.js
  9. 0 4
      protocols/luci-proto-hnet/htdocs/luci-static/resources/protocol/hnet.js
  10. 0 8
      protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/464xlat.js
  11. 0 8
      protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/6in4.js
  12. 0 8
      protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/6rd.js
  13. 0 8
      protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/6to4.js
  14. 0 14
      protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/dhcpv6.js
  15. 0 8
      protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/dslite.js
  16. 0 8
      protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/map.js
  17. 1 4
      protocols/luci-proto-modemmanager/htdocs/luci-static/resources/protocol/modemmanager.js
  18. 0 15
      protocols/luci-proto-ncm/htdocs/luci-static/resources/protocol/ncm.js
  19. 0 8
      protocols/luci-proto-openconnect/htdocs/luci-static/resources/protocol/openconnect.js
  20. 0 15
      protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/l2tp.js
  21. 0 15
      protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/ppp.js
  22. 0 15
      protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/pppoa.js
  23. 0 15
      protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/pppoe.js
  24. 0 15
      protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/pptp.js
  25. 0 15
      protocols/luci-proto-pppossh/htdocs/luci-static/resources/protocol/pppossh.js
  26. 0 16
      protocols/luci-proto-sstp/htdocs/luci-static/resources/protocol/sstp.js
  27. 0 4
      protocols/luci-proto-vpnc/htdocs/luci-static/resources/protocol/vpnc.js
  28. 0 5
      protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js

+ 2 - 7
applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/rules.js

@@ -193,13 +193,8 @@ return view.extend({
 		s.handleAdd = function(ev) {
 			var config_name = this.uciconfig || this.map.config,
 			    section_id = uci.add(config_name, this.sectiontype),
-			    opt1, opt2;
-
-			for (var i = 0; i < this.children.length; i++)
-				if (this.children[i].option == 'src')
-					opt1 = this.children[i];
-				else if (this.children[i].option == 'dest')
-					opt2 = this.children[i];
+			    opt1 = this.getOption('src'),
+			    opt2 = this.getOption('dest');
 
 			opt1.default = 'wan';
 			opt2.default = 'lan';

+ 2 - 3
applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/snats.js

@@ -217,9 +217,8 @@ return view.extend({
 		o.placeholder = null;
 		o.depends('target', 'SNAT');
 		o.validate = function(section_id, value) {
-			var port = this.map.lookupOption('snat_port', section_id),
-			    a = this.formvalue(section_id),
-			    p = port ? port[0].formvalue(section_id) : null;
+			var a = this.formvalue(section_id),
+			    p = this.section.formvalue(section_id, 'snat_port');
 
 			if ((a == null || a == '') && (p == null || p == '') && value == '')
 				return _('A rewrite IP must be specified!');

+ 0 - 15
modules/luci-base/htdocs/luci-static/resources/protocol/dhcp.js

@@ -34,21 +34,6 @@ return network.registerProtocol('dhcp', {
 		o = s.taboption('advanced', form.Flag, 'broadcast', _('Use broadcast flag'), _('Required for certain ISPs, e.g. Charter with DOCSIS 3'));
 		o.default = o.disabled;
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
-		o.depends('peerdns', '0');
-		o.datatype = 'ipaddr';
-		o.cast     = 'string';
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-
 		o = s.taboption('advanced', form.Value, 'clientid', _('Client ID to send when requesting DHCP'));
 		o.datatype  = 'hexstring';
 

+ 0 - 30
modules/luci-base/htdocs/luci-static/resources/protocol/static.js

@@ -179,28 +179,6 @@ return network.registerProtocol('static', {
 		s.taboption('general', this.CBINetmaskValue, 'netmask', _('IPv4 netmask'));
 		s.taboption('general', this.CBIGatewayValue, 'gateway', _('IPv4 gateway'));
 		s.taboption('general', this.CBIBroadcastValue, 'broadcast', _('IPv4 broadcast'));
-		s.taboption('general', form.DynamicList, 'dns', _('Use custom DNS servers'));
-
-		o = s.taboption('general', 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(64)';
-
-		o = s.taboption('general', 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 = s.taboption('general', form.DynamicList, 'ip6addr', _('IPv6 address'));
 		o.datatype = 'ip6addr';
@@ -215,10 +193,6 @@ return network.registerProtocol('static', {
 		o.datatype = 'ip6addr';
 		o.depends('ip6assign', '');
 
-		o = s.taboption('general', 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 = s.taboption('advanced', form.Value, 'macaddr', _('Override MAC address'));
 		o.datatype = 'macaddr';
 		o.placeholder = dev ? (dev.getMAC() || '') : '';
@@ -226,9 +200,5 @@ return network.registerProtocol('static', {
 		o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU'));
 		o.datatype = 'max(9200)';
 		o.placeholder = dev ? (dev.getMTU() || '1500') : '1500';
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = this.getMetric() || '0';
-		o.datatype = 'uinteger';
 	}
 });

+ 984 - 0
modules/luci-mod-network/htdocs/luci-static/resources/tools/network.js

@@ -0,0 +1,984 @@
+'use strict';
+'require ui';
+'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) {
+	var exists = false;
+
+	uci.sections('network', 'device', function(ss) {
+		exists = exists || (ss['.name'] != section_id && ss.name == devname /* && !ss.type*/);
+	});
+
+	/* Until http://lists.openwrt.org/pipermail/openwrt-devel/2020-July/030397.html lands,
+	   prevent redeclaring interface bridges */
+	if (!exists) {
+		var m = devname.match(/^br-([A-Za-z0-9_]+)$/),
+		    s = m ? uci.get('network', m[1]) : null;
+
+		if (s && s['.type'] == 'interface' && s.type == 'bridge')
+			exists = true;
+	}
+
+	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 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'),
+	    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) ||
+		uci.get('network', section_id, this.option) ||
+		this.default;
+}
+
+function deviceWrite(section_id, formvalue) {
+	var ds = lookupDevSection(this.section, section_id, true);
+
+	uci.set('network', ds, this.option, formvalue);
+	uci.unset('network', section_id, this.option);
+}
+
+function deviceRemove(section_id) {
+	var ds = lookupDevSection(this.section, section_id, false),
+	    sv = ds ? uci.get('network', ds) : null;
+
+	if (sv) {
+		var empty = true;
+
+		for (var opt in sv) {
+			if (opt.charAt(0) == '.' || opt == 'name' || opt == this.option)
+				continue;
+
+			empty = false;
+		}
+
+		if (empty)
+			uci.remove('network', ds);
+	}
+
+	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));
+	}
+}
+
+
+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);
+
+		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.cfgvalue = deviceCfgValue;
+			o.write = deviceWrite;
+			o.remove = deviceRemove;
+			o.refresh = deviceRefresh;
+		}
+
+		return o;
+	},
+
+	addDeviceOptions: function(s, dev, isNew) {
+		var isIface = (s.sectiontype == 'interface'),
+		    ifc = isIface ? network.instantiateNetwork(s.section) : null,
+		    gensection = ifc ? 'physical' : 'devgeneral',
+		    advsection = ifc ? 'physical' : 'devadvanced',
+		    simpledep = ifc ? { type: '', ifname_single: /^[^@]/ } : { type: '' },
+		    o, ss;
+
+		if (isIface) {
+			var type;
+
+			type = this.addOption(s, gensection, 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 protoname = this.section.formvalue(section_id, 'proto'),
+				    protocol = network.getProtocol(protoname),
+				    new_ifnames = this.isActive(section_id) ? L.toArray(this.section.formvalue(section_id, value ? 'ifname_multi' : 'ifname_single')) : [];
+
+				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());
+
+				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');
+			};
+		}
+		else {
+			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, gensection, 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, gensection, 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);
+			};
+			o.validate = function(section_id, value) {
+				return deviceSectionExists(section_id, value) ? _('A configuration for the device "%s" already exists').format(value) : true;
+			};
+			o.depends('type', '');
+		}
+
+		o = this.addOption(s, gensection, widgets.DeviceSelect, 'ifname_single', isIface ? _('Interface') : _('Base device'));
+		o.readonly = !isNew;
+		o.rmempty = false;
+		o.noaliases = !isIface;
+		o.default = (dev ? dev.getName() : '').match(/^.+\.\d+$/) ? dev.getName().replace(/\.\d+$/, '') : '';
+		o.ucioption = 'ifname';
+		o.validate = function(section_id, value) {
+			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;
+		};
+		if (isIface) {
+			o.write = o.remove = function() {};
+			o.cfgvalue = function(section_id) {
+				return (ifc.getDevices() || L.toArray(ifc.getDevice())).map(function(dev) {
+					return dev.getName();
+				});
+			};
+			o.onchange = function(ev, section_id, values) {
+				for (var i = 0, co; (co = this.section.children[i]) != null; i++)
+					if (co !== this && co.refresh)
+						co.refresh(section_id);
+
+			};
+			o.depends('type', '');
+		}
+		else {
+			o.write = o.remove = setIfActive;
+			o.depends('type', '8021q');
+			o.depends('type', '8021ad');
+			o.depends('type', 'macvlan');
+		}
+
+		o = this.addOption(s, gensection, 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, gensection, 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');
+
+		if (!isIface) {
+			o = this.addOption(s, gensection, 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, advsection, 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, advsection, 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, gensection, widgets.DeviceSelect, 'ifname_multi', _('Bridge ports'));
+		o.size = 10;
+		o.rmempty = true;
+		o.multiple = true;
+		o.noaliases = true;
+		o.nobridges = true;
+		o.ucioption = 'ifname';
+		if (isIface) {
+			o.write = o.remove = function() {};
+			o.cfgvalue = function(section_id) {
+				return (ifc.getDevices() || L.toArray(ifc.getDevice())).map(function(dev) { return dev.getName() });
+			};
+		}
+		else {
+			o.write = o.remove = setIfActive;
+			o.default = L.toArray(dev ? dev.getPorts() : null).filter(function(p) { return p.getType() != 'wifi' || p.isUp() }).map(function(p) { return p.getName() });
+			o.filter = function(section_id, device_name) {
+				var d = network.instantiateDevice(device_name);
+				return d.getType() != 'wifi' || d.isUp();
+			};
+		}
+		o.onchange = function(ev, section_id, values) {
+			ss.updatePorts(values);
+
+			return ss.parse().then(function() {
+				ss.redraw();
+			});
+		};
+		o.depends('type', 'bridge');
+
+		o = this.addOption(s, gensection, 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.addOption(s, advsection, form.Value, 'priority', _('Priority'));
+		o.placeholder = '32767';
+		o.datatype = 'range(0, 65535)';
+		o.depends('type', 'bridge');
+
+		o = this.addOption(s, advsection, 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.addOption(s, advsection, 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.addOption(s, advsection, 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.addOption(s, advsection, 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.addOption(s, advsection, 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.addOption(s, advsection, 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.addOption(s, advsection, form.Value, 'hash_max', _('Maximum snooping table size'));
+		o.placeholder = '512';
+		o.datatype = 'uinteger';
+		o.depends({ type: 'bridge', igmp_snooping: '1' });
+
+		o = this.addOption(s, advsection, form.Flag, 'multicast_querier', _('Enable multicast querier'));
+		o.defaults = { '1': [{'igmp_snooping': '1'}], '0': [{'igmp_snooping': '0'}] };
+		o.depends('type', 'bridge');
+
+		o = this.addOption(s, advsection, 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.addOption(s, advsection, 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.addOption(s, advsection, 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.addOption(s, advsection, 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, gensection, form.Value, 'mtu', _('MTU'));
+		o.placeholder = getDeviceValue(ifc || dev, 'getMTU');
+		o.datatype = 'max(9200)';
+		o.depends(simpledep);
+
+		o = this.addOption(s, gensection, form.Value, 'macaddr', _('MAC address'));
+		o.placeholder = getDeviceValue(ifc || dev, 'getMAC');
+		o.datatype = 'macaddr';
+		o.depends(simpledep);
+		o.depends('type', 'macvlan');
+		o.depends('type', 'veth');
+
+		o = this.addOption(s, gensection, 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, gensection, form.Value, 'peer_macaddr', _('Peer MAC address'));
+		o.rmempty = true;
+		o.datatype = 'macaddr';
+		o.depends('type', 'veth');
+
+		o = this.addOption(s, gensection, form.Value, 'txqueuelen', _('TX queue length'));
+		o.placeholder = dev ? dev._devstate('qlen') : '';
+		o.datatype = 'uinteger';
+		o.depends(simpledep);
+
+		o = this.addOption(s, advsection, form.Flag, 'promisc', _('Enable promiscious mode'));
+		o.default = o.disabled;
+		o.depends(simpledep);
+
+		o = this.addOption(s, advsection, 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(simpledep);
+
+		o = this.addOption(s, advsection, form.Flag, 'acceptlocal', _('Accept local'), _('Accept packets with local source addresses'));
+		o.default = o.disabled;
+		o.depends(simpledep);
+
+		o = this.addOption(s, advsection, form.Flag, 'sendredirects', _('Send ICMP redirects'));
+		o.default = o.enabled;
+		o.depends(simpledep);
+
+		o = this.addOption(s, advsection, form.Value, 'neighreachabletime', _('Neighbour cache validity'), _('Time in milliseconds'));
+		o.placeholder = '30000';
+		o.datatype = 'uinteger';
+		o.depends(simpledep);
+
+		o = this.addOption(s, advsection, form.Value, 'neighgcstaletime', _('Stale neighbour cache timeout'), _('Timeout in seconds'));
+		o.placeholder = '60';
+		o.datatype = 'uinteger';
+		o.depends(simpledep);
+
+		o = this.addOption(s, advsection, 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(simpledep);
+
+		o = this.addOption(s, gensection, form.Flag, 'ipv6', _('Enable IPv6'));
+		o.default = o.enabled;
+		o.depends(simpledep);
+
+		o = this.addOption(s, gensection, form.Value, 'mtu6', _('IPv6 MTU'));
+		o.placeholder = getDeviceValue(ifc || dev, 'getMTU');
+		o.datatype = 'max(9200)';
+		o.depends(Object.assign({ ipv6: '1' }, simpledep));
+
+		o = this.addOption(s, gensection, 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' }, simpledep));
+
+
+		o = this.addOption(s, advsection, form.Flag, 'multicast', _('Enable multicast support'));
+		o.default = o.enabled;
+		o.depends(simpledep);
+
+		o = this.addOption(s, advsection, 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' }, simpledep));
+
+		o = this.addOption(s, advsection, 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' }, simpledep));
+
+		if (isBridgePort(dev)) {
+			o = this.addOption(s, 'brport', form.Flag, 'learning', _('Enable MAC address learning'));
+			o.default = o.enabled;
+			o.depends(simpledep);
+
+			o = this.addOption(s, 'brport', form.Flag, 'unicast_flood', _('Enable unicast flooding'));
+			o.default = o.enabled;
+			o.depends(simpledep);
+
+			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(simpledep);
+
+			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' }, simpledep));
+
+			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' }, simpledep));
+
+			o = this.addOption(s, 'brport', form.Flag, 'multicast_fast_leave', _('Enable multicast fast leave'));
+			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);
+	}
+});

+ 346 - 106
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;
 
@@ -212,21 +213,19 @@ 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;
 }
 
 return view.extend({
@@ -293,14 +292,24 @@ return view.extend({
 	load: function() {
 		return Promise.all([
 			network.getDSLModemType(),
+			network.getDevices(),
+			fs.lines('/etc/iproute2/rt_tables'),
 			uci.changes()
 		]);
 	},
 
 	render: function(data) {
 		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 +332,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'));
 
@@ -413,80 +424,6 @@ return view.extend({
 				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,14 +467,6 @@ 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')) {
@@ -610,9 +539,9 @@ return view.extend({
 					};
 
 					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));
+						var uielem = this.getUIElement(section_id);
+						if (uielem)
+							uielem.setPlaceholder(get_netmask(s, false));
 						return form.Value.prototype.validate.apply(this, [ section_id, value ]);
 					};
 
@@ -660,25 +589,127 @@ return view.extend({
 				}
 
 				ifc.renderFormOptions(s);
+				nettools.addDeviceOptions(s, null, true);
+
+				// 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;
+
+				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'));
+				o.depends('peerdns', '0');
+				o.datatype = 'ipaddr';
+
+				o = nettools.replaceOption(s, 'advanced', form.DynamicList, 'dns_search', _('DNS search domains'));
+				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 'ifname_multi':
+					case 'ifname_single':
+					case 'igmp_snooping':
+					case 'stp':
+					case 'type':
+						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,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_'),
@@ -793,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'))
@@ -865,14 +911,208 @@ 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': []
+		};
 
-			this.default = (protoval == 'static') ? this.enabled : this.disabled;
-			return this.super('render', [ option_index, section_id, in_table ]);
+
+		// 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:(.+)$/);
+
+			if (m) {
+				var devtype = getDevType(section_id);
+
+				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.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(/* ... */) {
+			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');
+
+			return netDevs.filter(function(d) { return d.getName() == name })[0];
+		}
+
+		function getDevType(section_id) {
+			var cfgtype = uci.get('network', section_id, 'type'),
+			    dev = getDevice(section_id);
+
+			switch (cfgtype || (dev ? dev.getType() : '')) {
+			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:/);
+
+			return E('span', {
+				'class': 'ifacebadge',
+				'style': ext ? 'opacity:.5' : null
+			}, [
+				render_iface(dev), ' ', 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;

+ 1 - 0
modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json

@@ -4,6 +4,7 @@
 		"read": {
 			"cgi-io": [ "exec" ],
 			"file": {
+				"/etc/iproute2/rt_tables": [ "read" ],
 				"/usr/libexec/luci-peeraddr": [ "exec" ]
 			},
 			"ubus": {

+ 0 - 15
protocols/luci-proto-3g/htdocs/luci-static/resources/protocol/3g.js

@@ -113,21 +113,6 @@ return network.registerProtocol('3g', {
 		o.placeholder = '10';
 		o.datatype    = 'min(1)';
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-		o.depends('defaultroute', '1');
-
-		o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
-		o.depends('peerdns', '0');
-		o.datatype = 'ipaddr';
-
 		o = s.taboption('advanced', form.Value, '_keepalive_failure', _('LCP echo failure threshold'), _('Presume peer to be dead after given amount of LCP echo failures, use 0 to ignore failures'));
 		o.placeholder = '0';
 		o.datatype    = 'uinteger';

+ 0 - 4
protocols/luci-proto-hnet/htdocs/luci-static/resources/protocol/hnet.js

@@ -24,10 +24,6 @@ return network.registerProtocol('hnet', {
 		o.value('hybrid', _('Hybrid'));
 		o.default = 'auto';
 
-		o = s.taboption('advanced', form.Value, 'ip6assign', _('IPv6 assignment length'), _('Assign a part of given length of every public IPv6-prefix to this interface'));
-		o.datatype = 'max(128)';
-		o.default = '64';
-
 		s.taboption('advanced', form.Value, 'link_id', _('IPv6 assignment hint'), _('Assign prefix parts using this hexadecimal subprefix ID for this interface.'));
 
 		o = s.taboption('advanced', form.Value, 'ip4assign', _('IPv4 assignment length'));

+ 0 - 8
protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/464xlat.js

@@ -45,14 +45,6 @@ return network.registerProtocol('464xlat', {
 		o.nocreate = true;
 		o.exclude  = s.section;
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-		o.depends('defaultroute', '1');
-
 		o = s.taboption('advanced', form.Value, 'mtu', _('Use MTU on tunnel interface'));
 		o.placeholder = '1280';
 		o.datatype    = 'max(9200)';

+ 0 - 8
protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/6in4.js

@@ -82,14 +82,6 @@ return network.registerProtocol('6in4', {
 		o.password = true;
 		o.depends('_update', '1');
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-		o.depends('defaultroute', '1');
-
 		o = s.taboption('advanced', form.Value, 'ttl', _('Use TTL on tunnel interface'));
 		o.placeholder = '64';
 		o.datatype    = 'range(1,255)';

+ 0 - 8
protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/6rd.js

@@ -62,14 +62,6 @@ return network.registerProtocol('6rd', {
 		o.placeholder = '0';
 		o.datatype    = 'range(0,32)';
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-		o.depends('defaultroute', '1');
-
 		o = s.taboption('advanced', form.Value, 'ttl', _('Use TTL on tunnel interface'));
 		o.placeholder = '64';
 		o.datatype    = 'range(1,255)';

+ 0 - 8
protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/6to4.js

@@ -46,14 +46,6 @@ return network.registerProtocol('6to4', {
 			}, this));
 		};
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-		o.depends('defaultroute', '1');
-
 		o = s.taboption('advanced', form.Value, 'ttl', _('Use TTL on tunnel interface'));
 		o.placeholder = '64';
 		o.datatype    = 'range(1,255)';

+ 0 - 14
protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/dhcpv6.js

@@ -30,20 +30,6 @@ return network.registerProtocol('dhcpv6', {
 		o.value('64');
 		o.default = 'auto';
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.DynamicList, 'ip6prefix', _('Custom delegated IPv6-prefix'));
-		o.datatype = 'cidr6';
-
-		o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
-		o.depends('peerdns', '0');
-		o.datatype = 'ipaddr';
-		o.cast     = 'string';
-
 		o = s.taboption('advanced', form.Value, 'clientid', _('Client ID to send when requesting DHCP'));
 		o.datatype  = 'hexstring';
 

+ 0 - 8
protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/dslite.js

@@ -64,14 +64,6 @@ return network.registerProtocol('dslite', {
 		for (var i = 0; i < 256; i++)
 			o.value(i);
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-		o.depends('defaultroute', '1');
-
 		o = s.taboption('advanced', form.Value, 'mtu', _('Use MTU on tunnel interface'));
 		o.placeholder = '1280';
 		o.datatype    = 'max(9200)';

+ 0 - 8
protocols/luci-proto-ipv6/htdocs/luci-static/resources/protocol/map.js

@@ -77,14 +77,6 @@ return network.registerProtocol('map', {
 		o.nocreate = true;
 		o.exclude  = s.section;
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-		o.depends('defaultroute', '1');
-
 		o = s.taboption('advanced', form.Value, 'ttl', _('Use TTL on tunnel interface'));
 		o.placeholder = '64';
 		o.datatype    = 'range(1,255)';

+ 1 - 4
protocols/luci-proto-modemmanager/htdocs/luci-static/resources/protocol/modemmanager.js

@@ -120,11 +120,8 @@ return network.registerProtocol('modemmanager', {
 		o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU'));
 		o.placeholder = dev ? (dev.getMTU() || '1500') : '1500';
 		o.datatype    = 'max(9200)';
-		
+
 		o = s.taboption('general', form.Value, 'signalrate', _('Signal Refresh Rate'), _("In seconds"));
 		o.datatype = 'uinteger';
-		
-		s.taboption('general', form.Value, 'metric', _('Gateway metric'));
-
 	}
 });

+ 0 - 15
protocols/luci-proto-ncm/htdocs/luci-static/resources/protocol/ncm.js

@@ -104,20 +104,5 @@ return network.registerProtocol('ncm', {
 		o = s.taboption('advanced', form.Value, 'delay', _('Modem init timeout'), _('Maximum amount of seconds to wait for the modem to become ready'));
 		o.placeholder = '10';
 		o.datatype    = 'min(1)';
-
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-		o.depends('defaultroute', '1');
-
-		o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
-		o.depends('peerdns', '0');
-		o.datatype = 'ipaddr';
 	}
 });

+ 0 - 8
protocols/luci-proto-openconnect/htdocs/luci-static/resources/protocol/openconnect.js

@@ -153,14 +153,6 @@ return network.registerProtocol('openconnect', {
 			return callSetCertificateFiles(section_id, null, null, sanitizeCert(value));
 		};
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-		o.depends('defaultroute', '1');
-
 		o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU'));
 		o.optional = true;
 		o.placeholder = 1406;

+ 0 - 15
protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/l2tp.js

@@ -53,21 +53,6 @@ return network.registerProtocol('l2tp', {
 			o.default = 'auto';
 		}
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
-		o.depends('peerdns', '0');
-		o.datatype = 'ipaddr';
-		o.cast     = 'string';
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-
 		o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU'));
 		o.placeholder = dev ? (dev.getMTU() || '1500') : '1500';
 		o.datatype    = 'max(9200)';

+ 0 - 15
protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/ppp.js

@@ -97,21 +97,6 @@ return network.registerProtocol('ppp', {
 			o.default = 'auto';
 		}
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
-		o.depends('peerdns', '0');
-		o.datatype = 'ipaddr';
-		o.cast     = 'string';
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-
 		o = s.taboption('advanced', form.Value, '_keepalive_failure', _('LCP echo failure threshold'), _('Presume peer to be dead after given amount of LCP echo failures, use 0 to ignore failures'));
 		o.placeholder = '0';
 		o.datatype    = 'uinteger';

+ 0 - 15
protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/pppoa.js

@@ -84,21 +84,6 @@ return network.registerProtocol('pppoa', {
 			o.default = 'auto';
 		}
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
-		o.depends('peerdns', '0');
-		o.datatype = 'ipaddr';
-		o.cast     = 'string';
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-
 		o = s.taboption('advanced', form.Value, '_keepalive_failure', _('LCP echo failure threshold'), _('Presume peer to be dead after given amount of LCP echo failures, use 0 to ignore failures'));
 		o.placeholder = '0';
 		o.datatype    = 'uinteger';

+ 0 - 15
protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/pppoe.js

@@ -58,21 +58,6 @@ return network.registerProtocol('pppoe', {
 			o.default = 'auto';
 		}
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
-		o.depends('peerdns', '0');
-		o.datatype = 'ipaddr';
-		o.cast     = 'string';
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-
 		o = s.taboption('advanced', form.Value, '_keepalive_failure', _('LCP echo failure threshold'), _('Presume peer to be dead after given amount of LCP echo failures, use 0 to ignore failures'));
 		o.placeholder = '0';
 		o.datatype    = 'uinteger';

+ 0 - 15
protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/pptp.js

@@ -71,21 +71,6 @@ return network.registerProtocol('pptp', {
 			o.default = 'auto';
 		}
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
-		o.depends('peerdns', '0');
-		o.datatype = 'ipaddr';
-		o.cast     = 'string';
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-
 		o = s.taboption('advanced', form.Value, '_keepalive_failure', _('LCP echo failure threshold'), _('Presume peer to be dead after given amount of LCP echo failures, use 0 to ignore failures'));
 		o.placeholder = '0';
 		o.datatype    = 'uinteger';

+ 0 - 15
protocols/luci-proto-pppossh/htdocs/luci-static/resources/protocol/pppossh.js

@@ -94,21 +94,6 @@ return network.registerProtocol('pppossh', {
 			o.default = o.disabled;
 		}
 
-		o = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
-		o.depends('peerdns', '0');
-		o.datatype = 'ipaddr';
-		o.cast     = 'string';
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-
 		o = s.taboption('advanced', form.Value, '_keepalive_failure', _('LCP echo failure threshold'), _('Presume peer to be dead after given amount of LCP echo failures, use 0 to ignore failures'));
 		o.placeholder = '0';
 		o.datatype    = 'uinteger';

+ 0 - 16
protocols/luci-proto-sstp/htdocs/luci-static/resources/protocol/sstp.js

@@ -58,22 +58,6 @@ return network.registerProtocol('sstp', {
 		o.value('4', _('4', 'sstp log level value'));
 		o.default = '0';
 
-		var defaultroute = s.taboption('advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
-		defaultroute.default = defaultroute.enabled;
-
-		o = s.taboption('advanced', form.Value, 'metric', _('Use gateway metric'));
-		o.placeholder = '0';
-		o.datatype    = 'uinteger';
-		o.depends('defaultroute', defaultroute.enabled);
-
-		o = s.taboption('advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
-		o.default = o.enabled;
-
-		o = s.taboption('advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
-		o.depends('peerdns', '0');
-		o.datatype = 'ipaddr';
-		o.cast     = 'string';
-
 		o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU'));
 		o.placeholder = dev ? (dev.getMTU() || '1500') : '1500';
 		o.datatype    = 'max(9200)';

+ 0 - 4
protocols/luci-proto-vpnc/htdocs/luci-static/resources/protocol/vpnc.js

@@ -103,9 +103,5 @@ return network.registerProtocol('vpnc', {
 		o = s.taboption('general', form.Value, 'target_network', _('Target network'));
 		o.placeholder = '0.0.0.0/0';
 		o.datatype    = 'network';
-
-		o = s.taboption('general', form.ListValue, 'defaultroute', _('Default Route'), _('Set VPN as Default Route'));
-		o.value('0', _('No'));
-		o.value('1', _('Yes'));
 	}
 });

+ 0 - 5
protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js

@@ -89,11 +89,6 @@ return network.registerProtocol('wireguard', {
 
 		// -- advanced --------------------------------------------------------------------
 
-		o = s.taboption('advanced', form.Value, 'metric', _('Metric'), _('Optional'));
-		o.datatype = 'uinteger';
-		o.placeholder = '0';
-		o.optional = true;
-
 		o = s.taboption('advanced', form.Value, 'mtu', _('MTU'), _('Optional. Maximum Transmission Unit of tunnel interface.'));
 		o.datatype = 'range(1280,1420)';
 		o.placeholder = '1420';