'use strict';
+'require view';
+'require dom';
+'require poll';
'require fs';
'require ui';
'require uci';
'require network';
'require firewall';
'require tools.widgets as widgets';
+'require tools.network as nettools';
+
+var isReadonlyView = !L.hasViewPermission() || null;
function count_changes(section_id) {
var changes = ui.changes.changes, n = 0;
function render_modal_status(node, ifc) {
var dev = ifc ? (ifc.getDevice() || ifc.getL3Device() || ifc.getL3Device()) : null;
- L.dom.content(node, [
+ dom.content(node, [
E('img', {
'src': L.resource('icons/%s%s.png').format(dev ? dev.getType() : 'ethernet', (dev && dev.isUp()) ? '' : '_disabled'),
'title': dev ? dev.getTypeI18n() : _('Not present')
c.push(E('small', {}, ifc.isAlias() ? _('Alias of "%s"').format(ifc.isAlias())
: (dev ? dev.getName() : E('em', _('Not present')))));
- L.dom.content(node, c);
+ dom.content(node, c);
return firewall.getZoneByNetwork(ifc.getName()).then(L.bind(function(zone) {
this.style.backgroundColor = zone ? zone.getColor() : '#EEEEEE';
'class': 'cbi-button cbi-button-negative important',
'click': function(ev) {
dsc.setAttribute('disconnect', '');
- L.dom.content(dsc, E('em', _('Interface is shutting down...')));
+ dom.content(dsc, E('em', _('Interface is shutting down...')));
ui.hideModal();
}
}
else {
dsc.setAttribute('disconnect', '');
- L.dom.content(dsc, E('em', _('Interface is shutting down...')));
+ dom.content(dsc, E('em', _('Interface is shutting down...')));
}
});
}
else {
dsc.setAttribute(up ? 'reconnect' : 'disconnect', force ? 'force' : '');
- L.dom.content(dsc, E('em', up ? _('Interface is reconnecting...') : _('Interface is shutting down...')));
+ dom.content(dsc, E('em', up ? _('Interface is reconnecting...') : _('Interface is shutting down...')));
}
}
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 L.view.extend({
+return view.extend({
poll_status: function(map, networks) {
var resolveZone = null;
dynamic = ifc ? ifc.isDynamic() : false;
if (dsc.hasAttribute('reconnect')) {
- L.dom.content(dsc, E('em', _('Interface is starting...')));
+ dom.content(dsc, E('em', _('Interface is starting...')));
}
else if (dsc.hasAttribute('disconnect')) {
- L.dom.content(dsc, E('em', _('Interface is stopping...')));
+ dom.content(dsc, E('em', _('Interface is stopping...')));
}
else if (ifc.getProtocol() || uci.get('network', ifc.getName()) == null) {
render_status(dsc, ifc, false);
if (e) e.disabled = true;
var link = L.url('admin/system/opkg') + '?query=luci-proto';
- L.dom.content(dsc, [
+ dom.content(dsc, [
E('em', _('Unsupported protocol type.')), E('br'),
E('a', { href: link }, _('Install protocol extensions...'))
]);
}
else {
- L.dom.content(dsc, E('em', _('Interface not present or not connected yet.')));
+ dom.content(dsc, E('em', _('Interface not present or not connected yet.')));
}
if (stat) {
var dev = ifc.getDevice();
- L.dom.content(stat, [
+ dom.content(stat, [
E('img', {
'src': L.resource('icons/%s%s.png').format(dev ? dev.getType() : 'ethernet', (dev && dev.isUp()) ? '' : '_disabled'),
'title': dev ? dev.getTypeI18n() : _('Not present')
]);
}
- btn1.disabled = btn1.classList.contains('spinning') || btn2.classList.contains('spinning') || dynamic;
- btn2.disabled = btn1.classList.contains('spinning') || btn2.classList.contains('spinning') || dynamic || disabled;
+ btn1.disabled = isReadonlyView || btn1.classList.contains('spinning') || btn2.classList.contains('spinning') || dynamic;
+ btn2.disabled = isReadonlyView || btn1.classList.contains('spinning') || btn2.classList.contains('spinning') || dynamic || disabled;
}
return Promise.all([ resolveZone, network.flushCache() ]);
load: function() {
return Promise.all([
network.getDSLModemType(),
+ network.getDevices(),
+ fs.lines('/etc/iproute2/rt_tables'),
+ fs.read('/usr/lib/opkg/info/netifd.control'),
uci.changes()
]);
},
+ interfaceBridgeWithIfnameSections: function() {
+ return uci.sections('network', 'interface').filter(function(ns) {
+ return ns.type == 'bridge' && !ns.ports && ns.ifname;
+ });
+ },
+
+ deviceWithIfnameSections: function() {
+ return uci.sections('network', 'device').filter(function(ns) {
+ return ns.type == 'bridge' && !ns.ports && ns.ifname;
+ });
+ },
+
+ interfaceWithIfnameSections: function() {
+ return uci.sections('network', 'interface').filter(function(ns) {
+ return !ns.device && ns.ifname;
+ });
+ },
+
+ handleBridgeMigration: function(ev) {
+ var tasks = [];
+
+ this.interfaceBridgeWithIfnameSections().forEach(function(ns) {
+ var device_name = 'br-' + ns['.name'];
+
+ tasks.push(uci.callAdd('network', 'device', null, {
+ 'name': device_name,
+ 'type': 'bridge',
+ 'ports': L.toArray(ns.ifname)
+ }));
+
+ tasks.push(uci.callSet('network', ns['.name'], {
+ 'type': '',
+ 'ifname': '',
+ 'device': device_name
+ }));
+ });
+
+ return Promise.all(tasks)
+ .then(L.bind(ui.changes.init, ui.changes))
+ .then(L.bind(ui.changes.apply, ui.changes));
+ },
+
+ renderBridgeMigration: function() {
+ ui.showModal(_('Network bridge configuration migration'), [
+ E('p', _('The existing network configuration needs to be changed for LuCI to function properly.')),
+ E('p', _('Upon pressing "Continue", bridges configuration will be updated and the network will be restarted to apply the updated configuration.')),
+ E('div', { 'class': 'right' },
+ E('button', {
+ 'class': 'btn cbi-button-action important',
+ 'click': ui.createHandlerFn(this, 'handleBridgeMigration')
+ }, _('Continue')))
+ ]);
+ },
+
+ handleIfnameMigration: function(ev) {
+ var tasks = [];
+
+ this.deviceWithIfnameSections().forEach(function(ds) {
+ tasks.push(uci.add('network', ds['.name'], {
+ 'ifname': '',
+ 'ports': L.toArray(ds.ifname)
+ }));
+ });
+
+ this.interfaceWithIfnameSections().forEach(function(ns) {
+ tasks.push(uci.callSet('network', ns['.name'], {
+ 'ifname': '',
+ 'device': ns.ifname
+ }));
+ });
+
+ return Promise.all(tasks)
+ .then(L.bind(ui.changes.init, ui.changes))
+ .then(L.bind(ui.changes.apply, ui.changes));
+ },
+
+ renderIfnameMigration: function() {
+ ui.showModal(_('Network ifname configuration migration'), [
+ E('p', _('The existing network configuration needs to be changed for LuCI to function properly.')),
+ E('p', _('Upon pressing "Continue", ifname options will get renamed and the network will be restarted to apply the updated configuration.')),
+ E('div', { 'class': 'right' },
+ E('button', {
+ 'class': 'btn cbi-button-action important',
+ 'click': ui.createHandlerFn(this, 'handleIfnameMigration')
+ }, _('Continue')))
+ ]);
+ },
+
render: function(data) {
+ var netifdVersion = (data[3] || '').match(/Version: ([^\n]+)/);
+
+ if (netifdVersion && netifdVersion[1] >= "2021-05-26") {
+ if (this.interfaceBridgeWithIfnameSections().length)
+ return this.renderBridgeMigration();
+ else if (this.deviceWithIfnameSections().length || this.interfaceWithIfnameSections().length)
+ return this.renderIfnameMigration();
+ }
+
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');
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'));
disabled = net ? !net.isUp() : true,
dynamic = net ? net.isDynamic() : false;
- L.dom.content(tdEl.lastChild, [
+ dom.content(tdEl.lastChild, [
E('button', {
'class': 'cbi-button cbi-button-neutral reconnect',
'click': iface_updown.bind(this, true, section_id),
s.addModalOptions = function(s) {
var protoval = uci.get('network', s.section, 'proto'),
protoclass = protoval ? network.getProtocol(protoval) : null,
- o, ifname_single, ifname_multi, proto_select, proto_switch, type, stp, igmp, ss, so;
+ o, proto_select, proto_switch, type, stp, igmp, ss, so;
if (!protoval)
return;
}, this);
o.write = function() {};
+ o = s.taboption('general', widgets.DeviceSelect, 'device', _('Device'));
+ o.nobridges = false;
+ o.optional = false;
+ o.network = ifc.getName();
+
proto_select = s.taboption('general', form.ListValue, 'proto', _('Protocol'));
proto_select.modalonly = true;
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>create</em> field to define a new zone and attach the interface to it.'));
+ 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();
o.optional = true;
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')) {
};
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 ]);
};
- ss.taboption('advanced', form.DynamicList, 'dhcp_option', _('DHCP-Options'), _('Define additional DHCP options, for example "<code>6,192.168.2.1,192.168.2.2</code>" which advertises different DNS servers to clients.'));
+ ss.taboption('advanced', form.DynamicList, 'dhcp_option', _('DHCP-Options'), _('Define additional DHCP options, \
+ for example "<code>6,192.168.2.1,192.168.2.2</code>" which advertises different DNS servers to clients.'));
for (var i = 0; i < ss.children.length; i++)
if (ss.children[i].option != 'ignore')
ss.children[i].depends('ignore', '0');
- so = ss.taboption('ipv6', form.ListValue, 'ra', _('Router Advertisement-Service'));
+ so = ss.taboption('ipv6', form.ListValue, 'ra', _('<abbr title="Router Advertisement">RA</abbr>-Service'), _('<ul style="list-style-type:none;">\
+ <li><strong>server mode</strong>: Router advertises itself as the default IPv6 gateway \
+ via <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages \
+ (to <code>ff02::1</code>) and provides <abbr title="Prefix Delegation">PD</abbr> to downstream devices.</li>\
+ <li><strong>relay mode</strong>: Router relays <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> from upstream, \
+ and extends upstream (e.g. WAN) interface config and prefix to downstream (e.g. LAN) interfaces.</li>\
+ <li><strong>hybrid mode</strong>: Router does both server+relay; extends upstream config and prefix downstream, and \
+ uses <abbr title="Prefix Delegation">PD</abbr> locally.</li></ul>'));
so.value('', _('disabled'));
so.value('server', _('server mode'));
so.value('relay', _('relay mode'));
so.value('hybrid', _('hybrid mode'));
- so = ss.taboption('ipv6', form.ListValue, 'dhcpv6', _('DHCPv6-Service'));
+ so = ss.taboption('ipv6', form.Value, 'ra_maxinterval', _('Max <abbr title="Router Advertisement">RA</abbr> interval'), _('Maximum time allowed \
+ between sending unsolicited <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr>. Default is 600 seconds (<code>600</code>).'));
+ so.optional = true;
+ so.placeholder = '600';
+ so.depends('ra', 'server');
+ so.depends('ra', 'hybrid');
+ so.depends('ra', 'relay');
+
+
+ so = ss.taboption('ipv6', form.Value, 'ra_mininterval', _('Min <abbr title="Router Advertisement">RA</abbr> interval'), _('Minimum time allowed \
+ between sending unsolicited <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr>. Default is 200 seconds (<code>200</code>).'));
+ so.optional = true;
+ so.placeholder = '200';
+ so.depends('ra', 'server');
+ so.depends('ra', 'hybrid');
+ so.depends('ra', 'relay');
+
+ so = ss.taboption('ipv6', form.Value, 'ra_lifetime', _('<abbr title="Router Advertisement">RA</abbr> Lifetime'), _('Router Lifetime published \
+ in <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages. Default is 1800 seconds (<code>1800</code>). \
+ Max 9000 seconds.'));
+ so.optional = true;
+ so.depends('ra', 'server');
+ so.depends('ra', 'hybrid');
+ so.depends('ra', 'relay');
+
+ so = ss.taboption('ipv6', form.Value, 'ra_mtu', _('<abbr title="Router Advertisement">RA</abbr> MTU'), _('The <abbr title="Maximum Transmission Unit">MTU</abbr> \
+ to be published in <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages. Default is 0 (<code>0</code>).\
+ Min 1280.'));
+ so.optional = true;
+ so.depends('ra', 'server');
+ so.depends('ra', 'hybrid');
+ so.depends('ra', 'relay');
+
+ so = ss.taboption('ipv6', form.Value, 'ra_hoplimit', _('<abbr title="Router Advertisement">RA</abbr> Hop Limit'), _('The maximum hops \
+ to be published in <abbr title="Router Advertisement">RA</abbr> messages.<br />Default is 0 (<code>0</code>), meaning unspecified.\
+ Max 255.'));
+ so.optional = true;
+ so.depends('ra', 'server');
+ so.depends('ra', 'hybrid');
+ so.depends('ra', 'relay');
+
+ so = ss.taboption('ipv6', form.ListValue, 'ra_management', _('DHCPv6-Mode'), _('Default is stateless + stateful<br />\
+ <ul style="list-style-type:none;">\
+ <li><strong>stateless</strong>: Router advertises prefixes, host uses <abbr title="Stateless Address Auto Config">SLAAC</abbr> \
+ to self assign its own address. No DHCPv6.</li>\
+ <li><strong>stateless + stateful</strong>: SLAAC. In addition, router assigns an IPv6 address to a host via DHCPv6.</li>\
+ <li><strong>stateful-only</strong>: No SLAAC. Router assigns an IPv6 address to a host via DHCPv6.</li></ul>'));
+ so.value('0', _('stateless'));
+ so.value('1', _('stateless + stateful'));
+ so.value('2', _('stateful-only'));
+ so.depends('dhcpv6', 'server');
+ so.depends('dhcpv6', 'hybrid');
+ so.default = '1';
+
+ so = ss.taboption('ipv6', form.ListValue, 'dhcpv6', _('DHCPv6-Service'), _('<ul style="list-style-type:none;">\
+ <li><strong>server mode</strong>: Router assigns IPs and delegates prefixes \
+ (<abbr title="Prefix Delegation">PD</abbr>) to downstream interfaces.</li>\
+ <li><strong>relay mode</strong>: Router relays WAN interface config downstream. Helps support upstream \
+ links that lack <abbr title="Prefix Delegation">PD</abbr>.</li>\
+ <li><strong>hybrid mode</strong>: Router does combination of server+relay.</li></ul>'));
so.value('', _('disabled'));
so.value('server', _('server mode'));
so.value('relay', _('relay mode'));
so.value('hybrid', _('hybrid mode'));
- so = ss.taboption('ipv6', form.ListValue, 'ndp', _('NDP-Proxy'));
+ so = ss.taboption('ipv6', form.ListValue, 'ndp', _('<abbr title="Neighbour Discovery Protocol">NDP</abbr>-Proxy'), _('Reverts to \
+ disabled internally if there are no interfaces with boolean <code>ndproxy_slave</code> set to 1. Think of \
+ <abbr title="Neighbour Discovery Protocol">NDP</abbr> Proxy as Proxy ARP for IPv6: unify hosts on different physical \
+ hardware segments into the same IP subnet. Consists of <abbr title="Neighbour Solicitation, Type 135">NS</abbr> and \
+ <abbr title="Neighbour Advertisement, Type 136">NA</abbr> messages. <abbr title="Neighbour Discovery Protocol">NDP</abbr>-Proxy \
+ listens for <abbr title="Neighbour Solicitation, Type 135">NS</abbr> on an interface marked with boolean \
+ <code>master</code> as 1 (i.e. upstream), then queries the slave/internal interfaces for that target IP before finally \
+ sending an <abbr title="Neighbour Advertisement, Type 136">NA</abbr> message. \
+ <abbr title="Neighbour Discovery Protocol">NDP</abbr> is effectively ARP for IPv6. \
+ <abbr title="Neighbour Solicitation, Type 135">NS</abbr> and <abbr title="Neighbour Advertisement, Type 136">NA</abbr> \
+ detect reachability and duplicate addresses on a link, themselves also a prerequisite for SLAAC autoconfig.<br />\
+ <ul style="list-style-type:none;">\
+ <li><strong>disabled</strong>: No <abbr title="Neighbour Discovery Protocol">NDP</abbr> messages are proxied through to \
+ <code>ndproxy_slave</code> true interfaces.</li> \
+ <li><strong>relay mode</strong>: Proxies <abbr title="Neighbour Discovery Protocol">NDP</abbr> messages from <code>master</code> to \
+ <code>ndproxy_slave</code> true interfaces. Helps to support provider links without \
+ <abbr title="Prefix Delegation">PD</abbr>, and to firewall proxied hosts.</li>\
+ <li><strong>hybrid mode</strong>: Relay mode is disabled unless the interface boolean <code>master</code> is 1.</li></ul>'));
so.value('', _('disabled'));
so.value('relay', _('relay mode'));
so.value('hybrid', _('hybrid mode'));
+ so = ss.taboption('ipv6', form.Flag, 'ndproxy_routing', _('Learn routes from NDP'), _('Default is on.'));
+ so.default = '1';
+ so.optional = true;
+
+ so = ss.taboption('ipv6', form.Flag, 'ndproxy_slave', _('NDP-Proxy slave'), _('Set interface as NDP-Proxy external slave. Default is off.'));
+
+ so = ss.taboption('ipv6', form.DynamicList, 'ndproxy_static', _('Static NDP-Proxy prefixes'));
+
so = ss.taboption('ipv6', form.Flag , 'master', _('Master'), _('Set this interface as master for the dhcpv6 relay.'));
so.depends('dhcpv6', 'relay');
so.depends('dhcpv6', 'hybrid');
- so = ss.taboption('ipv6', form.ListValue, 'ra_management', _('DHCPv6-Mode'), _('Default is stateless + stateful'));
- so.value('0', _('stateless'));
- so.value('1', _('stateless + stateful'));
- so.value('2', _('stateful-only'));
- so.depends('dhcpv6', 'server');
+ so = ss.taboption('ipv6', form.Flag , 'master', _('Master'), _('Set this interface as master for the dhcpv6 relay.'));
+ so.depends('dhcpv6', 'relay');
so.depends('dhcpv6', 'hybrid');
- so.default = '1';
- so = ss.taboption('ipv6', form.Flag, 'ra_default', _('Always announce default router'), _('Announce as default router even if no public prefix is available.'));
+ so = ss.taboption('ipv6', form.Flag, 'ra_default', _('Announce as default router'), _('Always, even if no public prefix is available.'));
so.depends('ra', 'server');
so.depends('ra', 'hybrid');
ifc.renderFormOptions(s);
+ // 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;
+
+ if (protoval != 'static') {
+ 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'));
+ if (protoval != 'static')
+ o.depends('peerdns', '0');
+ o.datatype = 'ipaddr';
+
+ o = nettools.replaceOption(s, 'advanced', form.DynamicList, 'dns_search', _('DNS search domains'));
+ if (protoval != 'static')
+ 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 'device':
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 '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++)
o.depends('proto', protoval);
}
}
+
+ this.activeSection = s.section;
}, this));
};
+ s.handleModalCancel = function(/* ... */) {
+ var type = uci.get('network', this.activeSection || this.addedSection, 'type'),
+ device = (type == 'bridge') ? 'br-%s'.format(this.activeSection || this.addedSection) : null;
+
+ uci.sections('network', 'bridge-vlan', function(bvs) {
+ if (device != null && bvs.device == device)
+ 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_'),
protocols = network.getProtocols(),
- proto, name, bridge, ifname_single, ifname_multi;
+ proto, name, device;
protocols.sort(function(a, b) {
return a.getProtocol() > b.getProtocol();
proto = s2.option(form.ListValue, 'proto', _('Protocol'));
proto.validate = name.validate;
- bridge = s2.option(form.Flag, 'type', _('Bridge interfaces'), _('Creates a bridge over specified interface(s)'));
- bridge.modalonly = true;
- bridge.disabled = '';
- bridge.enabled = 'bridge';
-
- ifname_single = s2.option(widgets.DeviceSelect, 'ifname_single', _('Interface'));
- ifname_single.noaliases = false;
- ifname_single.optional = false;
-
- ifname_multi = s2.option(widgets.DeviceSelect, 'ifname_multi', _('Interface'));
- ifname_multi.nobridges = true;
- ifname_multi.noaliases = true;
- ifname_multi.multiple = true;
- ifname_multi.optional = true;
- ifname_multi.display_size = 6;
+ device = s2.option(widgets.DeviceSelect, 'device', _('Device'));
+ device.noaliases = false;
+ device.optional = false;
for (var i = 0; i < protocols.length; i++) {
proto.value(protocols[i].getProtocol(), protocols[i].getI18n());
- if (!protocols[i].isVirtual()) {
- bridge.depends({ proto: protocols[i].getProtocol() });
- ifname_single.depends({ type: '', proto: protocols[i].getProtocol() });
- ifname_multi.depends({ type: 'bridge', proto: protocols[i].getProtocol() });
- }
+ if (!protocols[i].isVirtual())
+ device.depends('proto', protocols[i].getProtocol());
}
m2.render().then(L.bind(function(nodes) {
'click': ui.createHandlerFn(this, function(ev) {
var nameval = name.isValid('_new_') ? name.formvalue('_new_') : null,
protoval = proto.isValid('_new_') ? proto.formvalue('_new_') : null,
- protoclass = protoval ? network.getProtocol(protoval) : null;
+ protoclass = protoval ? network.getProtocol(protoval, nameval) : null;
if (nameval == null || protoval == null || nameval == '' || protoval == '')
return;
return m.save(function() {
var section_id = uci.add('network', 'interface', nameval);
- uci.set('network', section_id, 'proto', protoval);
+ protoclass.set('proto', protoval);
+ protoclass.addDevice(device.formvalue('_new_'));
- if (ifname_single.isActive('_new_')) {
- uci.set('network', section_id, 'ifname', ifname_single.formvalue('_new_'));
- }
- else if (ifname_multi.isActive('_new_')) {
- uci.set('network', section_id, 'type', 'bridge');
- uci.set('network', section_id, 'ifname', L.toArray(ifname_multi.formvalue('_new_')).join(' '));
- }
+ m.children[0].addedSection = section_id;
}).then(L.bind(m.children[0].renderMoreOptionsModal, m.children[0], nameval));
-
});
})
}, _('Create interface'))
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': []
+ };
+
+
+ // 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;
- this.default = (protoval == 'static') ? this.enabled : this.disabled;
- return this.super('render', [ option_index, section_id, in_table ]);
+ 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:/),
+ icon = render_iface(dev);
+
+ if (ext)
+ icon.querySelector('img').style.opacity = '.5';
+
+ return E('span', { 'class': 'ifacebadge' }, [
+ icon,
+ E('span', { 'style': ext ? 'opacity:.5' : null }, [
+ 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;
s.anonymous = true;
- o = s.option(form.Value, 'ula_prefix', _('IPv6 ULA-Prefix'));
+ o = s.option(form.Value, 'ula_prefix', _('IPv6 ULA-Prefix'), _('Unique Local Address - in the range <code>fc00::/7</code>. \
+ Typically only within the ‘local’ half <code>fd00::/8</code>. ULA for IPv6 is analogous to IPv4 private network addressing.\
+ This prefix is randomly generated at first install.'));
o.datatype = 'cidr6';
o = s.option(form.Flag, 'packet_steering', _('Packet Steering'), _('Enable packet steering across all CPUs. May help or hinder network speed.'));
return m.render().then(L.bind(function(m, nodes) {
- L.Poll.add(L.bind(function() {
+ poll.add(L.bind(function() {
var section_ids = m.children[0].cfgsections(),
tasks = [];