diff --git a/luci-app-mptcp/luasrc/model/cbi/mptcp.lua b/luci-app-mptcp/luasrc/model/cbi/mptcp.lua
index 4b3bdb2b7..a2385142a 100755
--- a/luci-app-mptcp/luasrc/model/cbi/mptcp.lua
+++ b/luci-app-mptcp/luasrc/model/cbi/mptcp.lua
@@ -73,6 +73,9 @@ o.default = 1
--o:depends("mptcp_scheduler","roundrobin")
s = m:section(TypedSection, "interface", translate("Interfaces Settings"))
+function s.filter(self, section)
+ return not section:match("^oip.*") and not section:match("^lo.*") and section ~= "omrvpn" and section ~= "omr6in4"
+end
o = s:option(ListValue, "multipath", translate("Multipath TCP"), translate("One interface must be set as master"))
o:value("on", translate("enabled"))
o:value("off", translate("disabled"))
diff --git a/luci-mod-network/htdocs/luci-static/resources/tools/network.js b/luci-mod-network/htdocs/luci-static/resources/tools/network.js
new file mode 100644
index 000000000..311242905
--- /dev/null
+++ b/luci-mod-network/htdocs/luci-static/resources/tools/network.js
@@ -0,0 +1,934 @@
+'use strict';
+'require ui';
+'require dom';
+'require uci';
+'require form';
+'require network';
+'require baseclass';
+'require validation';
+'require tools.widgets as widgets';
+
+function validateAddr(section_id, value) {
+ if (value == '')
+ return true;
+
+ var ipv6 = /6$/.test(this.section.formvalue(section_id, 'mode')),
+ addr = ipv6 ? validation.parseIPv6(value) : validation.parseIPv4(value);
+
+ return addr ? true : (ipv6 ? _('Expecting a valid IPv6 address') : _('Expecting a valid IPv4 address'));
+}
+
+function 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
+ );
+ });
+
+ return exists;
+}
+
+function isBridgePort(dev) {
+ if (!dev)
+ return false;
+
+ if (dev.isBridgePort())
+ return true;
+
+ var isPort = false;
+
+ uci.sections('network', null, function(s) {
+ if (s['.type'] != 'interface' && s['.type'] != 'device')
+ return;
+
+ if (s.type == 'bridge' && L.toArray(s.ifname).indexOf(dev.getName()) > -1)
+ isPort = true;
+ });
+
+ return isPort;
+}
+
+function updateDevBadge(node, dev) {
+ var type = dev.getType(),
+ up = dev.getCarrier();
+
+ dom.content(node, [
+ E('img', {
+ 'class': 'middle',
+ 'src': L.resource('icons/%s%s.png').format(type, up ? '' : '_disabled')
+ }),
+ '\x0a', dev.getName()
+ ]);
+
+ return node;
+}
+
+function renderDevBadge(dev) {
+ return updateDevBadge(E('span', {
+ 'class': 'ifacebadge port-status-device',
+ 'style': 'font-weight:normal',
+ 'data-device': dev.getName()
+ }), dev);
+}
+
+function updatePortStatus(node, dev) {
+ var carrier = dev.getCarrier(),
+ duplex = dev.getDuplex(),
+ speed = dev.getSpeed(),
+ desc, title;
+
+ if (carrier && speed > 0 && duplex != null) {
+ desc = '%d%s'.format(speed, duplex == 'full' ? 'FD' : 'HD');
+ title = '%s, %d MBit/s, %s'.format(_('Connected'), speed, duplex == 'full' ? _('full-duplex') : _('half-duplex'));
+ }
+ else if (carrier) {
+ desc = _('Connected');
+ }
+ else {
+ desc = _('no link');
+ }
+
+ dom.content(node, [
+ E('img', {
+ 'class': 'middle',
+ 'src': L.resource('icons/port_%s.png').format(carrier ? 'up' : 'down')
+ }),
+ '\x0a', desc
+ ]);
+
+ if (title)
+ node.setAttribute('data-tooltip', title);
+ else
+ node.removeAttribute('data-tooltip');
+
+ return node;
+}
+
+function renderPortStatus(dev) {
+ return updatePortStatus(E('span', {
+ 'class': 'ifacebadge port-status-link',
+ 'data-device': dev.getName()
+ }), dev);
+}
+
+function updatePlaceholders(opt, section_id) {
+ var dev = network.instantiateDevice(opt.getUIElement(section_id).getValue());
+
+ for (var i = 0, co; (co = opt.section.children[i]) != null; i++) {
+ if (co !== opt) {
+ switch (co.option) {
+ case 'mtu':
+ case 'mtu6':
+ co.getUIElement(section_id).setPlaceholder(dev.getMTU());
+ break;
+
+ case 'macaddr':
+ co.getUIElement(section_id).setPlaceholder(dev.getMAC());
+ break;
+
+ case 'txqueuelen':
+ co.getUIElement(section_id).setPlaceholder(dev._devstate('qlen'));
+ break;
+ }
+ }
+ }
+}
+
+
+var cbiTagValue = form.Value.extend({
+ renderWidget: function(section_id, option_index, cfgvalue) {
+ var widget = new ui.Dropdown(cfgvalue || ['-'], {
+ '-': E([], [
+ E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '—' ]),
+ E('span', { 'class': 'hide-close' }, [ _('Do not participate', 'VLAN port state') ])
+ ]),
+ 'u': E([], [
+ E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 'u' ]),
+ E('span', { 'class': 'hide-close' }, [ _('Egress untagged', 'VLAN port state') ])
+ ]),
+ 't': E([], [
+ E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 't' ]),
+ E('span', { 'class': 'hide-close' }, [ _('Egress tagged', 'VLAN port state') ])
+ ]),
+ '*': E([], [
+ E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '*' ]),
+ E('span', { 'class': 'hide-close' }, [ _('Primary VLAN ID', 'VLAN port state') ])
+ ])
+ }, {
+ id: this.cbid(section_id),
+ sort: [ '-', 'u', 't', '*' ],
+ optional: false,
+ multiple: true
+ });
+
+ var field = this;
+
+ widget.toggleItem = function(sb, li, force_state) {
+ var lis = li.parentNode.querySelectorAll('li'),
+ toggle = ui.Dropdown.prototype.toggleItem;
+
+ toggle.apply(this, [sb, li, force_state]);
+
+ if (force_state != null)
+ return;
+
+ switch (li.getAttribute('data-value'))
+ {
+ case '-':
+ if (li.hasAttribute('selected')) {
+ for (var i = 0; i < lis.length; i++) {
+ switch (lis[i].getAttribute('data-value')) {
+ case '-':
+ break;
+
+ case '*':
+ toggle.apply(this, [sb, lis[i], false]);
+ lis[i].setAttribute('unselectable', '');
+ break;
+
+ default:
+ toggle.apply(this, [sb, lis[i], false]);
+ }
+ }
+ }
+ break;
+
+ case 't':
+ case 'u':
+ if (li.hasAttribute('selected')) {
+ for (var i = 0; i < lis.length; i++) {
+ switch (lis[i].getAttribute('data-value')) {
+ case li.getAttribute('data-value'):
+ break;
+
+ case '*':
+ lis[i].removeAttribute('unselectable');
+ break;
+
+ default:
+ toggle.apply(this, [sb, lis[i], false]);
+ }
+ }
+ }
+ else {
+ toggle.apply(this, [sb, li, true]);
+ }
+ break;
+
+ case '*':
+ if (li.hasAttribute('selected')) {
+ var section_ids = field.section.cfgsections();
+
+ for (var i = 0; i < section_ids.length; i++) {
+ var other_widget = field.getUIElement(section_ids[i]),
+ other_value = L.toArray(other_widget.getValue());
+
+ if (other_widget === this)
+ continue;
+
+ var new_value = other_value.filter(function(v) { return v != '*' });
+
+ if (new_value.length == other_value.length)
+ continue;
+
+ other_widget.setValue(new_value);
+ break;
+ }
+ }
+ }
+ };
+
+ var node = widget.render();
+
+ node.style.minWidth = '4em';
+
+ if (cfgvalue == '-')
+ node.querySelector('li[data-value="*"]').setAttribute('unselectable', '');
+
+ return E('div', { 'style': 'display:inline-block' }, node);
+ },
+
+ cfgvalue: function(section_id) {
+ var ports = L.toArray(uci.get('network', section_id, 'ports'));
+
+ for (var i = 0; i < ports.length; i++) {
+ var s = ports[i].split(/:/);
+
+ if (s[0] != this.port)
+ continue;
+
+ var t = /t/.test(s[1] || '') ? 't' : 'u';
+
+ return /\*/.test(s[1] || '') ? [t, '*'] : [t];
+ }
+
+ 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);
+ },
+
+ addDeviceOptions: function(s, dev, isNew) {
+ var parent_dev = dev ? dev.getParent() : null,
+ o, ss;
+
+ s.tab('devgeneral', _('General device options'));
+ s.tab('devadvanced', _('Advanced device options'));
+ s.tab('brport', _('Bridge port specific options'));
+ s.tab('bridgevlan', _('Bridge VLAN filtering'));
+
+ o = this.replaceOption(s, 'devgeneral', form.ListValue, 'type', _('Device type'));
+ o.readonly = !isNew;
+ o.value('', _('Network device'));
+ o.value('bridge', _('Bridge device'));
+ o.value('8021q', _('VLAN (802.1q)'));
+ o.value('8021ad', _('VLAN (802.1ad)'));
+ o.value('macvlan', _('MAC VLAN'));
+ o.value('veth', _('Virtual Ethernet'));
+ o.validate = function(section_id, value) {
+ if (value == 'bridge' || value == 'veth')
+ updatePlaceholders(this.section.getOption('name_complex'), section_id);
+
+ return true;
+ };
+
+ o = this.replaceOption(s, 'devgeneral', widgets.DeviceSelect, 'name_simple', _('Existing device'));
+ o.readonly = !isNew;
+ o.rmempty = false;
+ o.noaliases = true;
+ o.default = (dev ? dev.getName() : '');
+ o.ucioption = 'name';
+ o.filter = function(section_id, value) {
+ var dev = network.instantiateDevice(value);
+ return !deviceSectionExists(section_id, value) && (dev.getType() != 'wifi' || dev.isUp());
+ };
+ o.validate = function(section_id, value) {
+ updatePlaceholders(this, section_id);
+
+ return deviceSectionExists(section_id, value)
+ ? _('A configuration for the device "%s" already exists').format(value) : true;
+ };
+ o.onchange = function(ev, section_id, values) {
+ updatePlaceholders(this, section_id);
+ };
+ o.depends('type', '');
+
+ o = this.replaceOption(s, 'devgeneral', widgets.DeviceSelect, 'ifname_single', _('Base device'));
+ o.readonly = !isNew;
+ o.rmempty = false;
+ o.noaliases = true;
+ o.default = (dev ? dev.getName() : '').match(/^.+\.\d+$/) ? dev.getName().replace(/\.\d+$/, '') : '';
+ o.ucioption = 'ifname';
+ o.filter = function(section_id, value) {
+ var dev = network.instantiateDevice(value);
+ return (dev.getType() != 'wifi' || dev.isUp());
+ };
+ o.validate = function(section_id, value) {
+ updatePlaceholders(this, section_id);
+
+ if (isNew) {
+ var type = this.section.formvalue(section_id, 'type'),
+ name = this.section.getUIElement(section_id, 'name_complex');
+
+ if (type == 'macvlan' && value && name && !name.isChanged()) {
+ var i = 0;
+
+ while (deviceSectionExists(section_id, '%smac%d'.format(value, i)))
+ i++;
+
+ name.setValue('%smac%d'.format(value, i));
+ name.triggerValidation();
+ }
+ }
+
+ return true;
+ };
+ o.onchange = function(ev, section_id, values) {
+ updatePlaceholders(this, section_id);
+ };
+ o.depends('type', '8021q');
+ o.depends('type', '8021ad');
+ o.depends('type', 'macvlan');
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'vid', _('VLAN ID'));
+ o.readonly = !isNew;
+ o.datatype = 'range(1, 4094)';
+ o.rmempty = false;
+ o.default = (dev ? dev.getName() : '').match(/^.+\.\d+$/) ? dev.getName().replace(/^.+\./, '') : '';
+ o.validate = function(section_id, value) {
+ var base = this.section.formvalue(section_id, 'ifname_single'),
+ vid = this.section.formvalue(section_id, 'vid'),
+ name = this.section.getUIElement(section_id, 'name_complex');
+
+ if (base && vid && name && !name.isChanged()) {
+ name.setValue('%s.%d'.format(base, vid));
+ name.triggerValidation();
+ }
+
+ return true;
+ };
+ o.depends('type', '8021q');
+ o.depends('type', '8021ad');
+
+ o = this.replaceOption(s, 'devgeneral', form.ListValue, 'mode', _('Mode'));
+ o.value('vepa', _('VEPA (Virtual Ethernet Port Aggregator)', 'MACVLAN mode'));
+ o.value('private', _('Private (Prevent communication between MAC VLANs)', 'MACVLAN mode'));
+ o.value('bridge', _('Bridge (Support direct communication between MAC VLANs)', 'MACVLAN mode'));
+ o.value('passthru', _('Pass-through (Mirror physical device to single MAC VLAN)', 'MACVLAN mode'));
+ o.depends('type', 'macvlan');
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'name_complex', _('Device name'));
+ o.rmempty = false;
+ o.datatype = 'maxlength(15)';
+ o.readonly = !isNew;
+ o.ucioption = 'name';
+ o.validate = function(section_id, value) {
+ var dev = network.instantiateDevice(value);
+
+ if (deviceSectionExists(section_id, value) || (isNew && (dev.dev || {}).idx))
+ return _('The device name "%s" is already taken').format(value);
+
+ return true;
+ };
+ o.depends({ type: '', '!reverse': true });
+
+ o = this.replaceOption(s, 'devadvanced', form.DynamicList, 'ingress_qos_mapping', _('Ingress QoS mapping'), _('Defines a mapping of VLAN header priority to the Linux internal packet priority on incoming frames'));
+ o.rmempty = true;
+ o.validate = validateQoSMap;
+ o.depends('type', '8021q');
+ o.depends('type', '8021ad');
+
+ o = this.replaceOption(s, 'devadvanced', form.DynamicList, 'egress_qos_mapping', _('Egress QoS mapping'), _('Defines a mapping of Linux internal packet priority to VLAN header priority but for outgoing frames'));
+ o.rmempty = true;
+ o.validate = validateQoSMap;
+ o.depends('type', '8021q');
+ o.depends('type', '8021ad');
+
+ o = this.replaceOption(s, 'devgeneral', widgets.DeviceSelect, 'ifname_multi', _('Bridge ports'));
+ o.size = 10;
+ o.rmempty = true;
+ o.multiple = true;
+ o.noaliases = true;
+ o.nobridges = true;
+ o.ucioption = 'ports';
+ o.default = L.toArray(dev ? dev.getPorts() : null).filter(function(p) { return p.getType() != 'wifi' }).map(function(p) { return p.getName() });
+ o.filter = function(section_id, device_name) {
+ var bridge_name = uci.get('network', section_id, 'name'),
+ choice_dev = network.instantiateDevice(device_name),
+ parent_dev = choice_dev.getParent();
+
+ /* only show wifi networks which are already present in "option ifname" */
+ if (choice_dev.getType() == 'wifi') {
+ var ifnames = L.toArray(uci.get('network', section_id, 'ports'));
+
+ for (var i = 0; i < ifnames.length; i++)
+ if (ifnames[i] == device_name)
+ return true;
+
+ return false;
+ }
+
+ return (!parent_dev || parent_dev.getName() != bridge_name);
+ };
+ o.description = _('Specifies the wired ports to attach to this bridge. In order to attach wireless networks, choose the associated interface as network in the wireless settings.')
+ o.onchange = function(ev, section_id, values) {
+ ss.updatePorts(values);
+
+ return ss.parse().then(function() {
+ ss.redraw();
+ });
+ };
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devgeneral', form.Flag, 'bridge_empty', _('Bring up empty bridge'), _('Bring up the bridge interface even if no ports are attached'));
+ o.default = o.disabled;
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'priority', _('Priority'));
+ o.placeholder = '32767';
+ o.datatype = 'range(0, 65535)';
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'ageing_time', _('Ageing time'), _('Timeout in seconds for learned MAC addresses in the forwarding database'));
+ o.placeholder = '30';
+ o.datatype = 'uinteger';
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devadvanced', form.Flag, 'stp', _('Enable STP'), _('Enables the Spanning Tree Protocol on this bridge'));
+ o.default = o.disabled;
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'hello_time', _('Hello interval'), _('Interval in seconds for STP hello packets'));
+ o.placeholder = '2';
+ o.datatype = 'range(1, 10)';
+ o.depends({ type: 'bridge', stp: '1' });
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'forward_delay', _('Forward delay'), _('Time in seconds to spend in listening and learning states'));
+ o.placeholder = '15';
+ o.datatype = 'range(2, 30)';
+ o.depends({ type: 'bridge', stp: '1' });
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'max_age', _('Maximum age'), _('Timeout in seconds until topology updates on link loss'));
+ o.placeholder = '20';
+ o.datatype = 'range(6, 40)';
+ o.depends({ type: 'bridge', stp: '1' });
+
+
+ o = this.replaceOption(s, 'devadvanced', form.Flag, 'igmp_snooping', _('Enable IGMP snooping'), _('Enables IGMP snooping on this bridge'));
+ o.default = o.disabled;
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'hash_max', _('Maximum snooping table size'));
+ o.placeholder = '512';
+ o.datatype = 'uinteger';
+ o.depends({ type: 'bridge', igmp_snooping: '1' });
+
+ o = this.replaceOption(s, 'devadvanced', form.Flag, 'multicast_querier', _('Enable multicast querier'));
+ o.defaults = { '1': [{'igmp_snooping': '1'}], '0': [{'igmp_snooping': '0'}] };
+ o.depends('type', 'bridge');
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'robustness', _('Robustness'), _('The robustness value allows tuning for the expected packet loss on the network. If a network is expected to be lossy, the robustness value may be increased. IGMP is robust to (Robustness-1) packet losses'));
+ o.placeholder = '2';
+ o.datatype = 'min(1)';
+ o.depends({ type: 'bridge', multicast_querier: '1' });
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'query_interval', _('Query interval'), _('Interval in centiseconds between multicast general queries. By varying the value, an administrator may tune the number of IGMP messages on the subnet; larger values cause IGMP Queries to be sent less often'));
+ o.placeholder = '12500';
+ o.datatype = 'uinteger';
+ o.depends({ type: 'bridge', multicast_querier: '1' });
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'query_response_interval', _('Query response interval'), _('The max response time in centiseconds inserted into the periodic general queries. By varying the value, an administrator may tune the burstiness of IGMP messages on the subnet; larger values make the traffic less bursty, as host responses are spread out over a larger interval'));
+ o.placeholder = '1000';
+ o.datatype = 'uinteger';
+ o.validate = function(section_id, value) {
+ var qiopt = L.toArray(this.map.lookupOption('query_interval', section_id))[0],
+ qival = qiopt ? (qiopt.formvalue(section_id) || qiopt.placeholder) : '';
+
+ if (value != '' && qival != '' && +value >= +qival)
+ return _('The query response interval must be lower than the query interval value');
+
+ return true;
+ };
+ o.depends({ type: 'bridge', multicast_querier: '1' });
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'last_member_interval', _('Last member interval'), _('The max response time in centiseconds inserted into group-specific queries sent in response to leave group messages. It is also the amount of time between group-specific query messages. This value may be tuned to modify the "leave latency" of the network. A reduced value results in reduced time to detect the loss of the last member of a group'));
+ o.placeholder = '100';
+ o.datatype = 'uinteger';
+ o.depends({ type: 'bridge', multicast_querier: '1' });
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'mtu', _('MTU'));
+ o.datatype = 'range(576, 9200)';
+ o.validate = function(section_id, value) {
+ var parent_mtu = (dev && dev.getType() == 'vlan') ? (parent_dev ? parent_dev.getMTU() : null) : null;
+
+ if (parent_mtu !== null && +value > parent_mtu)
+ return _('The MTU must not exceed the parent device MTU of %d bytes').format(parent_mtu);
+
+ return true;
+ };
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'macaddr', _('MAC address'));
+ o.datatype = 'macaddr';
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'peer_name', _('Peer device name'));
+ o.rmempty = true;
+ o.datatype = 'maxlength(15)';
+ o.depends('type', 'veth');
+ o.load = function(section_id) {
+ var sections = uci.sections('network', 'device'),
+ idx = 0;
+
+ for (var i = 0; i < sections.length; i++)
+ if (sections[i]['.name'] == section_id)
+ break;
+ else if (sections[i].type == 'veth')
+ idx++;
+
+ this.placeholder = 'veth%d'.format(idx);
+
+ return form.Value.prototype.load.apply(this, arguments);
+ };
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'peer_macaddr', _('Peer MAC address'));
+ o.rmempty = true;
+ o.datatype = 'macaddr';
+ o.depends('type', 'veth');
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'txqueuelen', _('TX queue length'));
+ o.placeholder = dev ? dev._devstate('qlen') : '';
+ o.datatype = 'uinteger';
+
+ o = this.replaceOption(s, 'devadvanced', form.Flag, 'promisc', _('Enable promiscuous mode'));
+ o.default = o.disabled;
+
+ o = this.replaceOption(s, 'devadvanced', form.Flag, 'autoneg', _('Autonegociation'));
+ o.default = o.enabled;
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'speed', _('Speed'));
+ o.placeholder = dev ? dev.getSpeed() : '';
+ o.default = '';
+ o.rmempty = true;
+ o.datatype = 'uinteger';
+ o.depends('autoneg', '0');
+
+ o = this.replaceOption(s, 'devadvanced', form.ListValue, 'duplex', _('Duplex'));
+ o.default = '';
+ o.value('', _('unknown'));
+ o.value('half', _('half'));
+ o.value('full', _('full'));
+ o.depends('autoneg', '0');
+
+ o = this.replaceOption(s, 'devadvanced', form.ListValue, 'rpfilter', _('Reverse path filter'));
+ o.default = '';
+ o.value('', _('disabled'));
+ o.value('loose', _('Loose filtering'));
+ o.value('strict', _('Strict filtering'));
+ o.cfgvalue = function(section_id) {
+ var val = form.ListValue.prototype.cfgvalue.apply(this, [section_id]);
+
+ switch (val || '') {
+ case 'loose':
+ case '1':
+ return 'loose';
+
+ case 'strict':
+ case '2':
+ return 'strict';
+
+ default:
+ return '';
+ }
+ };
+
+ o = this.replaceOption(s, 'devadvanced', form.Flag, 'acceptlocal', _('Accept local'), _('Accept packets with local source addresses'));
+ o.default = o.disabled;
+
+ o = this.replaceOption(s, 'devadvanced', form.Flag, 'sendredirects', _('Send ICMP redirects'));
+ o.default = o.enabled;
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'neighreachabletime', _('Neighbour cache validity'), _('Time in milliseconds'));
+ o.placeholder = '30000';
+ o.datatype = 'uinteger';
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'neighgcstaletime', _('Stale neighbour cache timeout'), _('Timeout in seconds'));
+ o.placeholder = '60';
+ o.datatype = 'uinteger';
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'neighlocktime', _('Minimum ARP validity time'), _('Minimum required time in seconds before an ARP entry may be replaced. Prevents ARP cache thrashing.'));
+ o.placeholder = '0';
+ o.datatype = 'uinteger';
+
+ o = this.replaceOption(s, 'devadvanced', form.Value, 'ttl', _('Force TTL'), _('Some LTE providers detect tethering by inspecting packet TTL values'));
+ o.placeholder = '65';
+ o.datatype = 'uinteger';
+
+ o = this.replaceOption(s, 'devgeneral', form.Flag, 'ipv6', _('Enable IPv6'));
+ o.migrate = false;
+ o.default = o.enabled;
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'mtu6', _('IPv6 MTU'));
+ o.datatype = 'max(9200)';
+ o.depends('ipv6', '1');
+
+ o = this.replaceOption(s, 'devgeneral', form.Value, 'dadtransmits', _('DAD transmits'), _('Amount of Duplicate Address Detection probes to send'));
+ o.placeholder = '1';
+ o.datatype = 'uinteger';
+ o.depends('ipv6', '1');
+
+
+ o = this.replaceOption(s, 'devadvanced', form.Flag, 'multicast', _('Enable multicast support'));
+ o.default = o.enabled;
+
+ o = this.replaceOption(s, 'devadvanced', form.ListValue, 'igmpversion', _('Force IGMP version'));
+ o.value('', _('No enforcement'));
+ o.value('1', _('Enforce IGMPv1'));
+ o.value('2', _('Enforce IGMPv2'));
+ o.value('3', _('Enforce IGMPv3'));
+ o.depends('multicast', '1');
+
+ o = this.replaceOption(s, 'devadvanced', form.ListValue, 'mldversion', _('Force MLD version'));
+ o.value('', _('No enforcement'));
+ o.value('1', _('Enforce MLD version 1'));
+ o.value('2', _('Enforce MLD version 2'));
+ o.depends('multicast', '1');
+
+ if (isBridgePort(dev)) {
+ o = this.replaceOption(s, 'brport', form.Flag, 'learning', _('Enable MAC address learning'));
+ o.default = o.enabled;
+
+ o = this.replaceOption(s, 'brport', form.Flag, 'unicast_flood', _('Enable unicast flooding'));
+ o.default = o.enabled;
+
+ o = this.replaceOption(s, 'brport', form.Flag, 'isolated', _('Port isolation'), _('Only allow communication with non-isolated bridge ports when enabled'));
+ o.default = o.disabled;
+
+ o = this.replaceOption(s, 'brport', form.ListValue, 'multicast_router', _('Multicast routing'));
+ o.value('', _('Never'));
+ o.value('1', _('Learn'));
+ o.value('2', _('Always'));
+ o.depends('multicast', '1');
+
+ o = this.replaceOption(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('multicast', '1');
+
+ o = this.replaceOption(s, 'brport', form.Flag, 'multicast_fast_leave', _('Enable multicast fast leave'));
+ o.default = o.disabled;
+ o.depends('multicast', '1');
+ }
+
+ o = this.replaceOption(s, 'bridgevlan', form.Flag, 'vlan_filtering', _('Enable VLAN filtering'));
+ o.depends('type', 'bridge');
+ o.updateDefaultValue = function(section_id) {
+ var device = uci.get('network', s.section, 'name'),
+ uielem = this.getUIElement(section_id),
+ has_vlans = false;
+
+ uci.sections('network', 'bridge-vlan', function(bvs) {
+ has_vlans = has_vlans || (bvs.device == device);
+ });
+
+ this.default = has_vlans ? this.enabled : this.disabled;
+
+ if (uielem && !uielem.isChanged())
+ uielem.setValue(this.default);
+ };
+
+ o = this.replaceOption(s, 'bridgevlan', form.SectionValue, 'bridge-vlan', form.TableSection, 'bridge-vlan');
+ o.depends('type', 'bridge');
+
+ ss = o.subsection;
+ ss.addremove = true;
+ ss.anonymous = true;
+
+ ss.renderHeaderRows = function(/* ... */) {
+ var node = form.TableSection.prototype.renderHeaderRows.apply(this, arguments);
+
+ node.querySelectorAll('.th').forEach(function(th) {
+ th.classList.add('left');
+ th.classList.add('middle');
+ });
+
+ return node;
+ };
+
+ ss.filter = function(section_id) {
+ var devname = uci.get('network', s.section, 'name');
+ return (uci.get('network', section_id, 'device') == devname);
+ };
+
+ ss.render = function(/* ... */) {
+ return form.TableSection.prototype.render.apply(this, arguments).then(L.bind(function(node) {
+ node.style.overflow = 'auto hidden';
+ node.style.paddingTop = '1em';
+
+ if (this.node)
+ this.node.parentNode.replaceChild(node, this.node);
+
+ this.node = node;
+
+ return node;
+ }, this));
+ };
+
+ ss.redraw = function() {
+ return this.load().then(L.bind(this.render, this));
+ };
+
+ ss.updatePorts = function(ports) {
+ var devices = ports.map(function(port) {
+ return network.instantiateDevice(port)
+ }).filter(function(dev) {
+ return dev.getType() != 'wifi' || dev.isUp();
+ });
+
+ this.children = this.children.filter(function(opt) { return !opt.option.match(/^port_/) });
+
+ for (var i = 0; i < devices.length; i++) {
+ o = ss.option(cbiTagValue, 'port_%s'.format(sfh(devices[i].getName())), renderDevBadge(devices[i]), renderPortStatus(devices[i]));
+ o.port = devices[i].getName();
+ }
+
+ var section_ids = this.cfgsections(),
+ device_names = devices.reduce(function(names, dev) { names[dev.getName()] = true; return names }, {});
+
+ for (var i = 0; i < section_ids.length; i++) {
+ var old_spec = L.toArray(uci.get('network', section_ids[i], 'ports')),
+ new_spec = old_spec.filter(function(spec) { return device_names[spec.replace(/:[ut*]+$/, '')] });
+
+ if (old_spec.length != new_spec.length)
+ uci.set('network', section_ids[i], 'ports', new_spec.length ? new_spec : null);
+ }
+ };
+
+ ss.handleAdd = function(ev) {
+ return s.parse().then(L.bind(function() {
+ var device = uci.get('network', s.section, 'name'),
+ section_ids = this.cfgsections(),
+ section_id = null,
+ max_vlan_id = 0;
+
+ if (!device)
+ return;
+
+ for (var i = 0; i < section_ids.length; i++) {
+ var vid = +uci.get('network', section_ids[i], 'vlan');
+
+ if (vid > max_vlan_id)
+ max_vlan_id = vid;
+ }
+
+ section_id = uci.add('network', 'bridge-vlan');
+ uci.set('network', section_id, 'device', device);
+ uci.set('network', section_id, 'vlan', max_vlan_id + 1);
+
+ s.children.forEach(function(opt) {
+ switch (opt.option) {
+ case 'type':
+ case 'name_complex':
+ var input = opt.map.findElement('id', 'widget.%s'.format(opt.cbid(s.section)));
+ if (input)
+ input.disabled = true;
+ break;
+ }
+ });
+
+ s.getOption('vlan_filtering').updateDefaultValue(s.section);
+
+ s.map.addedVLANs = s.map.addedVLANs || [];
+ s.map.addedVLANs.push(section_id);
+
+ return this.redraw();
+ }, this));
+ };
+
+ o = ss.option(form.Value, 'vlan', _('VLAN ID'));
+ o.datatype = 'range(1, 4094)';
+
+ o.renderWidget = function(/* ... */) {
+ var node = form.Value.prototype.renderWidget.apply(this, arguments);
+
+ node.style.width = '5em';
+
+ return node;
+ };
+
+ o.validate = function(section_id, value) {
+ var section_ids = this.section.cfgsections();
+
+ for (var i = 0; i < section_ids.length; i++) {
+ if (section_ids[i] == section_id)
+ continue;
+
+ if (uci.get('network', section_ids[i], 'vlan') == value)
+ return _('The VLAN ID must be unique');
+ }
+
+ return true;
+ };
+
+ o = ss.option(form.Flag, 'local', _('Local'));
+ o.default = o.enabled;
+
+ var ports = [];
+
+ var seen_ports = {};
+
+ L.toArray(uci.get('network', s.section, 'ports')).forEach(function(port) {
+ seen_ports[port] = true;
+ });
+
+ uci.sections('network', 'bridge-vlan', function(bvs) {
+ if (uci.get('network', s.section, 'name') != bvs.device)
+ return;
+
+ L.toArray(bvs.ports).forEach(function(portspec) {
+ var m = portspec.match(/^([^:]+)(?::[ut*]+)?$/);
+
+ if (m)
+ seen_ports[m[1]] = true;
+ });
+ });
+
+ for (var port_name in seen_ports)
+ ports.push(port_name);
+
+ ports.sort(function(a, b) {
+ var m1 = a.match(/^(.+?)([0-9]*)$/),
+ m2 = b.match(/^(.+?)([0-9]*)$/);
+
+ if (m1[1] < m2[1])
+ return -1;
+ else if (m1[1] > m2[1])
+ return 1;
+ else
+ return +(m1[2] || 0) - +(m2[2] || 0);
+ });
+
+ ss.updatePorts(ports);
+ },
+
+ updateDevBadge: updateDevBadge,
+ updatePortStatus: updatePortStatus
+});