'use strict'; 'require fs'; '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 cbiFlagTristate = form.ListValue.extend({ __init__: function(/* ... */) { this.super('__init__', arguments); this.keylist = [ '', '0!', '1!' ]; this.vallist = [ _('automatic'), _('disabled'), _('enabled') ]; }, load: function(section_id) { var invert = false, sysfs = this.sysfs; if (sysfs) { if (sysfs.charAt(0) == '!') { invert = true; sysfs = sysfs.substring(1); } return L.resolveDefault(fs.read(sysfs), '').then(L.bind(function(res) { res = (res || '').trim(); if (res == '0') this.sysfs_default = invert; else if (res == '1') this.sysfs_default = !invert; return this.super('load', [section_id]); }, this)); } return this.super('load', [section_id]); }, write: function(section_id, formvalue) { if (formvalue == '1!') return this.super('write', [section_id, '1']); else if (formvalue == '0!') return this.super('write', [section_id, '0']); else return this.super('remove', [section_id]); }, renderWidget: function(section_id, option_index, cfgvalue) { var sysdef = this.sysfs_default; if (this.sysfs_default !== null) { this.keylist[0] = sysdef ? '1' : '0'; this.vallist[0] = sysdef ? _('automatic (enabled)') : _('automatic (disabled)'); } return this.super('renderWidget', [section_id, option_index, cfgvalue ? cfgvalue + '!' : null]); } }); 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' }, [ _('Not Member', 'VLAN port state') ]) ]), 'u': E([], [ E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 'U' ]), E('span', { 'class': 'hide-close' }, [ _('Untagged', 'VLAN port state') ]) ]), 't': E([], [ E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 'T' ]), E('span', { 'class': 'hide-close' }, [ _('Tagged', 'VLAN port state') ]) ]), '*': E([], [ E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '*' ]), E('span', { 'class': 'hide-close' }, [ _('Is Primary VLAN', '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 /\x2a/.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.length ? ports : null); }, 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, devname = dev ? dev.getName() : 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() && isNew) { 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', cbiFlagTristate, 'promisc', _('Enable promiscuous mode')); o.sysfs_default = (dev && dev.dev && dev.dev.flags) ? dev.dev.flags.promisc : null; 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(/* ... */) { var val = form.ListValue.prototype.cfgvalue.apply(this, arguments); switch (val || '') { case 'loose': case '1': return 'loose'; case 'strict': case '2': return 'strict'; default: return ''; } }; o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'acceptlocal', _('Accept local'), _('Accept packets with local source addresses')); o.sysfs = '/proc/sys/net/ipv4/conf/%s/accept_local'.format(devname || 'default'); o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'sendredirects', _('Send ICMP redirects')); o.sysfs = '/proc/sys/net/ipv4/conf/%s/send_redirects'.format(devname || 'default'); o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'arp_accept', _('Honor gratuitous ARP'), _('When enabled, new ARP table entries are added from received gratuitous ARP requests or replies, otherwise only preexisting table entries are updated, but no new hosts are learned.')); o.sysfs = '/proc/sys/net/ipv4/conf/%s/arp_accept'.format(devname || 'default'); o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'drop_gratuitous_arp', _('Drop gratuitous ARP'), _('Drop all gratuitous ARP frames, for example if there’s a known good ARP proxy on the network and such frames need not be used or in the case of 802.11, must not be used to prevent attacks.')); o.sysfs = '/proc/sys/net/ipv4/conf/%s/drop_gratuitous_arp'.format(devname || 'default'); 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, 'devgeneral', cbiFlagTristate, 'ipv6', _('Enable IPv6')); o.sysfs = '!/proc/sys/net/ipv6/conf/%s/disable_ipv6'.format(devname || 'default'); o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'ip6segmentrouting', _('Enable IPv6 segment routing')); o.sysfs = '/proc/sys/net/ipv6/conf/%s/seg6_enabled'.format(devname || 'default'); o.depends('ipv6', /1/); o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'drop_unsolicited_na', _('Drop unsolicited NA'), _('Drop all unsolicited neighbor advertisements, for example if there’s a known good NA proxy on the network and such frames need not be used or in the case of 802.11, must not be used to prevent attacks.')); o.sysfs = '/proc/sys/net/ipv6/conf/%s/drop_unsolicited_na'.format(devname || 'default'); o.depends('ipv6', /1/); 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', cbiFlagTristate, 'multicast', _('Enable multicast support')); o.sysfs_default = (dev && dev.dev && dev.dev.flags) ? dev.dev.flags.multicast : null; 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', cbiFlagTristate, 'learning', _('Enable MAC address learning')); o.sysfs = '/sys/class/net/%s/brport/learning'.format(devname || 'default'); o = this.replaceOption(s, 'brport', cbiFlagTristate, 'unicast_flood', _('Enable unicast flooding')); o.sysfs = '/sys/class/net/%s/brport/unicast_flood'.format(devname || 'default'); o = this.replaceOption(s, 'brport', cbiFlagTristate, 'isolate', _('Port isolation'), _('Only allow communication with non-isolated bridge ports when enabled')); o.sysfs = '/sys/class/net/%s/brport/isolated'.format(devname || 'default'); 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', cbiFlagTristate, 'multicast_to_unicast', _('Multicast to unicast'), _('Forward multicast packets as unicast packets on this device.')); o.sysfs = '/sys/class/net/%s/brport/multicast_to_unicast'.format(devname || 'default'); o.depends('multicast', /1/); o = this.replaceOption(s, 'brport', cbiFlagTristate, 'multicast_fast_leave', _('Enable multicast fast leave')); o.sysfs = '/sys/class/net/%s/brport/multicast_fast_leave'.format(devname || 'default'); o.depends('multicast', /1/); o = this.replaceOption(s, 'brport', cbiFlagTristate, 'drop_v4_unicast_in_l2_multicast', _('Drop nested IPv4 unicast'), _('Drop layer 2 multicast frames containing IPv4 unicast packets.')); o.sysfs = '/proc/sys/net/ipv4/conf/%s/drop_unicast_in_l2_multicast'.format(devname || 'default'); o.depends('multicast', /1/); o = this.replaceOption(s, 'brport', cbiFlagTristate, 'drop_v6_unicast_in_l2_multicast', _('Drop nested IPv6 unicast'), _('Drop layer 2 multicast frames containing IPv6 unicast packets.')); o.sysfs = '/proc/sys/net/ipv6/conf/%s/drop_unicast_in_l2_multicast'.format(devname || 'default'); 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(); }).sort(function(a, b) { return L.naturalCompare(a.getName(), b.getName()); }); 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)); }; ss.handleRemove = function(section_id) { this.map.data.remove('network', section_id); s.map.addedVLANs = (s.map.addedVLANs || []).filter(function(sid) { return sid != section_id; }); return this.redraw(); }; 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); ss.updatePorts(ports); }, updateDevBadge: updateDevBadge, updatePortStatus: updatePortStatus });