'use strict';
'require view';
'require dom';
'require poll';
'require fs';
'require ui';
'require uci';
'require form';
'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;
if (!L.isObject(changes))
return n;
if (Array.isArray(changes.network))
for (var i = 0; i < changes.network.length; i++)
n += (changes.network[i][1] == section_id);
if (Array.isArray(changes.dhcp))
for (var i = 0; i < changes.dhcp.length; i++)
n += (changes.dhcp[i][1] == section_id);
return n;
}
function render_iface(dev, alias) {
var type = dev ? dev.getType() : 'ethernet',
up = dev ? dev.isUp() : false;
return E('span', { class: 'cbi-tooltip-container' }, [
E('img', { 'class' : 'middle', 'src': L.resource('icons/%s%s.png').format(
alias ? 'alias' : type,
up ? '' : '_disabled') }),
E('span', { 'class': 'cbi-tooltip ifacebadge large' }, [
E('img', { 'src': L.resource('icons/%s%s.png').format(
type, up ? '' : '_disabled') }),
L.itemlist(E('span', { 'class': 'left' }), [
_('Type'), dev ? dev.getTypeI18n() : null,
_('Device'), dev ? dev.getName() : _('Not present'),
_('Connected'), up ? _('yes') : _('no'),
_('MAC'), dev ? dev.getMAC() : null,
_('RX'), dev ? '%.2mB (%d %s)'.format(dev.getRXBytes(), dev.getRXPackets(), _('Pkts.')) : null,
_('TX'), dev ? '%.2mB (%d %s)'.format(dev.getTXBytes(), dev.getTXPackets(), _('Pkts.')) : null
])
])
]);
}
function render_status(node, ifc, with_device) {
var desc = null, c = [];
if (ifc.isDynamic())
desc = _('Virtual dynamic interface');
else if (ifc.isAlias())
desc = _('Alias Interface');
else if (!uci.get('network', ifc.getName()))
return L.itemlist(node, [
null, E('em', _('Interface is marked for deletion'))
]);
desc = desc ? '%s (%s)'.format(desc, ifc.getI18n()) : ifc.getI18n();
const changecount = with_device ? 0 : count_changes(ifc.getName());
const maindev = ifc.getL3Device() || ifc.getDevice();
const macaddr = maindev ? maindev.getMAC() : null;
const cond00 = !changecount && !ifc.isDynamic() && !ifc.isAlias();
const cond01 = cond00 && macaddr;
const cond02 = cond00 && maindev;
function addEntries(label, array) {
return Array.isArray(array) ? array.flatMap((item) => [label, item]) : [label, null];
}
return L.itemlist(node, [
_('Protocol'), with_device ? null : (desc || '?'),
_('Device'), with_device ? (maindev ? maindev.getShortName() : E('em', _('Not present'))) : null,
_('Uptime'), (!changecount && ifc.isUp()) ? '%t'.format(ifc.getUptime()) : null,
_('MAC'), (cond01) ? macaddr : null,
_('RX'), (cond02) ? '%.2mB (%d %s)'.format(maindev.getRXBytes(), maindev.getRXPackets(), _('Pkts.')) : null,
_('TX'), (cond02) ? '%.2mB (%d %s)'.format(maindev.getTXBytes(), maindev.getTXPackets(), _('Pkts.')) : null,
...addEntries(_('IPv4'), changecount ? [] : ifc.getIPAddrs()),
...addEntries(_('IPv6'), changecount ? [] : ifc.getIP6Addrs()),
...addEntries(_('IPv6-PD'), changecount ? null : ifc.getIP6Prefixes?.()),
_('Information'), with_device ? null : (ifc.get('disabled') != '1' ? null : _('Interface disabled')),
_('Information'), with_device ? null : (ifc.get('auto') != '0' ? null : _('Not started on boot')),
...addEntries(_('Error'), ifc.getErrors()),
null, changecount ? E('a', {
href: '#',
click: L.bind(ui.changes.displayChanges, ui.changes)
}, _('Interface has %d pending changes').format(changecount)) : null
]);
}
function render_modal_status(node, ifc) {
var dev = ifc ? (ifc.getDevice() || ifc.getL3Device() || ifc.getL3Device()) : null;
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')
}),
ifc ? render_status(E('span'), ifc, true) : E('em', _('Interface not present or not connected yet.'))
]);
return node;
}
function render_ifacebox_status(node, ifc) {
var dev = ifc.getL3Device() || ifc.getDevice(),
subdevs = dev ? dev.getPorts() : null,
c = [ render_iface(dev, ifc.isAlias()) ];
if (subdevs && subdevs.length) {
var sifs = [ ' (' ];
for (var j = 0; j < subdevs.length; j++)
sifs.push(render_iface(subdevs[j]));
sifs.push(')');
c.push(E('span', {}, sifs));
}
c.push(E('br'));
c.push(E('small', {}, ifc.isAlias() ? _('Alias of "%s"').format(ifc.isAlias())
: (dev ? dev.getName() : E('em', _('Not present')))));
dom.content(node, c);
return firewall.getZoneByNetwork(ifc.getName()).then(L.bind(function(zone) {
this.style.backgroundColor = zone ? zone.getColor() : '#EEEEEE';
this.title = zone ? _('Part of zone %q').format(zone.getName()) : _('No zone assigned');
}, node.previousElementSibling));
}
function iface_updown(up, id, ev, force) {
var row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(id)),
dsc = row.querySelector('[data-name="_ifacestat"] > div'),
btns = row.querySelectorAll('.cbi-section-actions .reconnect, .cbi-section-actions .down');
btns[+!up].blur();
btns[+!up].classList.add('spinning');
btns[0].disabled = true;
btns[1].disabled = true;
if (!up) {
L.resolveDefault(fs.exec_direct('/usr/libexec/luci-peeraddr')).then(function(res) {
var info = null; try { info = JSON.parse(res); } catch(e) {}
if (L.isObject(info) &&
Array.isArray(info.inbound_interfaces) &&
info.inbound_interfaces.filter(function(i) { return i == id })[0]) {
ui.showModal(_('Confirm disconnect'), [
E('p', _('You appear to be currently connected to the device via the "%h" interface. Do you really want to shut down the interface?').format(id)),
E('div', { 'class': 'button-row' }, [
E('button', {
'class': 'btn cbi-button cbi-button-neutral',
'click': function(ev) {
btns[1].classList.remove('spinning');
btns[1].disabled = false;
btns[0].disabled = false;
ui.hideModal();
}
}, _('Cancel')),
' ',
E('button', {
'class': 'btn cbi-button cbi-button-negative important',
'click': function(ev) {
dsc.setAttribute('disconnect', '');
dom.content(dsc, E('em', _('Interface is shutting down...')));
ui.hideModal();
}
}, _('Disconnect'))
])
]);
}
else {
dsc.setAttribute('disconnect', '');
dom.content(dsc, E('em', _('Interface is shutting down...')));
}
});
}
else {
dsc.setAttribute(up ? 'reconnect' : 'disconnect', force ? 'force' : '');
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',
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 subnetmask = firstsubnet.split('/')[1];
if (!isNaN(subnetmask))
subnetmask = network.prefixToMask(+subnetmask);
return subnetmask;
}
function has_peerdns(proto) {
switch (proto) {
case 'dhcp':
case 'dhcpv6':
case 'qmi':
case 'ppp':
case 'pppoe':
case 'pppoa':
case 'pptp':
case 'openvpn':
case 'sstp':
return true;
}
return false;
}
function has_sourcefilter(proto) {
switch (proto) {
case '3g':
case 'dhcpv6':
case 'directip':
case 'mbim':
case 'modemmanager':
case 'ncm':
case 'ppp':
case 'pppoa':
case 'pppoe':
case 'pptp':
case 'qmi':
return true;
}
return false;
}
return view.extend({
poll_status: function(map, networks) {
var resolveZone = null;
for (var i = 0; i < networks.length; i++) {
var ifc = networks[i],
row = map.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(ifc.getName()));
if (row == null)
continue;
var dsc = row.querySelector('[data-name="_ifacestat"] > div'),
box = row.querySelector('[data-name="_ifacebox"] .ifacebox-body'),
btn1 = row.querySelector('.cbi-section-actions .reconnect'),
btn2 = row.querySelector('.cbi-section-actions .down'),
stat = document.querySelector('[id="%s-ifc-status"]'.format(ifc.getName())),
resolveZone = render_ifacebox_status(box, ifc),
disabled = ifc ? !ifc.isUp() : true,
dynamic = ifc ? ifc.isDynamic() : false;
if (dsc.hasAttribute('reconnect')) {
dom.content(dsc, E('em', _('Interface is starting...')));
}
else if (dsc.hasAttribute('disconnect')) {
dom.content(dsc, E('em', _('Interface is stopping...')));
}
else if (ifc.getProtocol() || uci.get('network', ifc.getName()) == null) {
render_status(dsc, ifc, false);
}
else if (!ifc.getProtocol()) {
var e = map.querySelector('[id="cbi-network-%s"] .cbi-button-edit'.format(ifc.getName()));
if (e) e.disabled = true;
var link = L.url('admin/system/opkg') + '?query=luci-proto';
dom.content(dsc, [
E('em', _('Unsupported protocol type.')), E('br'),
E('a', { href: link }, _('Install protocol extensions...'))
]);
}
else {
dom.content(dsc, E('em', _('Interface not present or not connected yet.')));
}
if (stat) {
var dev = ifc.getDevice();
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')
}),
render_status(E('span'), ifc, true)
]);
}
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;
}
document.querySelectorAll('.port-status-device[data-device]').forEach(function(node) {
nettools.updateDevBadge(node, network.instantiateDevice(node.getAttribute('data-device')));
});
document.querySelectorAll('.port-status-link[data-device]').forEach(function(node) {
nettools.updatePortStatus(node, network.instantiateDevice(node.getAttribute('data-device')));
});
return Promise.all([ resolveZone, network.flushCache() ]);
},
load: function() {
return Promise.all([
network.getDSLModemType(),
network.getDevices(),
fs.lines('/etc/iproute2/rt_tables'),
L.resolveDefault(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),
'mtu': ns.mtu,
'macaddr': ns.macaddr,
'igmp_snooping': ns.igmp_snooping
}));
tasks.push(uci.callSet('network', ns['.name'], {
'type': '',
'ifname': '',
'mtu': '',
'macaddr': '',
'igmp_snooping': '',
'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.callSet('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 = m.section(form.GridSection, 'interface', _('Interfaces'));
s.anonymous = true;
s.addremove = true;
s.addbtntitle = _('Add new interface...');
s.load = function() {
return Promise.all([
network.getNetworks(),
firewall.getZones(),
uci.load('system')
]).then(L.bind(function(data) {
this.networks = data[0];
this.zones = data[1];
}, this));
};
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'));
s.cfgsections = function() {
return this.networks.map(function(n) { return n.getName() })
.filter(function(n) { return n != 'loopback' });
};
s.modaltitle = function(section_id) {
return _('Interfaces') + ' » ' + section_id;
};
s.renderRowActions = function(section_id) {
var tdEl = this.super('renderRowActions', [ section_id, _('Edit') ]),
net = this.networks.filter(function(n) { return n.getName() == section_id })[0],
disabled = net ? !net.isUp() : true,
dynamic = net ? net.isDynamic() : false;
dom.content(tdEl.lastChild, [
E('button', {
'class': 'cbi-button cbi-button-neutral reconnect',
'click': iface_updown.bind(this, true, section_id),
'title': _('Reconnect this interface'),
'disabled': dynamic ? 'disabled' : null
}, _('Restart')),
E('button', {
'class': 'cbi-button cbi-button-neutral down',
'click': iface_updown.bind(this, false, section_id),
'title': _('Shutdown this interface'),
'disabled': (dynamic || disabled) ? 'disabled' : null
}, _('Stop')),
tdEl.lastChild.firstChild,
tdEl.lastChild.lastChild
]);
if (!dynamic && net && !uci.get('network', net.getName())) {
tdEl.lastChild.childNodes[0].disabled = true;
tdEl.lastChild.childNodes[2].disabled = true;
tdEl.lastChild.childNodes[3].disabled = true;
}
if (dynamic) {
//disable the 'Edit' button on dynamic interfaces
tdEl.lastChild.childNodes[2].disabled = true;
}
return tdEl;
};
s.addModalOptions = function(s) {
var protoval = uci.get('network', s.section, 'proto'),
protoclass = protoval ? network.getProtocol(protoval) : null,
o, proto_select, proto_switch, type, stp, igmp, ss, so;
if (!protoval)
return;
return network.getNetwork(s.section).then(L.bind(function(ifc) {
var protocols = network.getProtocols();
protocols.sort(function(a, b) {
return L.naturalCompare(a.getProtocol(), b.getProtocol());
});
o = s.taboption('general', form.DummyValue, '_ifacestat_modal', _('Status'));
o.modalonly = true;
o.cfgvalue = L.bind(function(section_id) {
var net = this.networks.filter(function(n) { return n.getName() == section_id })[0];
return render_modal_status(E('div', {
'id': '%s-ifc-status'.format(section_id),
'class': 'ifacebadge large'
}), net);
}, this);
o.write = function() {};
proto_select = s.taboption('general', form.ListValue, 'proto', _('Protocol'));
proto_select.modalonly = true;
proto_switch = s.taboption('general', form.Button, '_switch_proto');
proto_switch.modalonly = true;
proto_switch.title = _('Really switch protocol?');
proto_switch.inputtitle = _('Switch protocol');
proto_switch.inputstyle = 'apply';
proto_switch.onclick = L.bind(function(ev) {
s.map.save()
.then(L.bind(m.load, m))
.then(L.bind(m.render, m))
.then(L.bind(this.renderMoreOptionsModal, this, s.section));
}, this);
o = s.taboption('general', widgets.DeviceSelect, '_net_device', _('Device'));
o.ucioption = 'device';
o.nobridges = false;
o.optional = false;
o.network = ifc.getName();
o.exclude = '@' + ifc.getName();
o = s.taboption('general', form.Flag, 'disabled', _('Disable this interface'));
o.modalonly = true;
o = s.taboption('general', form.Flag, 'auto', _('Bring up on boot'));
o.modalonly = true;
o.default = o.enabled;
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 unspecified to remove the interface from the associated zone or fill out the custom field to define a new zone and attach the interface to it.'));
o.network = ifc.getName();
o.optional = true;
o.cfgvalue = function(section_id) {
return firewall.getZoneByNetwork(ifc.getName()).then(function(zone) {
return (zone != null ? zone.getName() : null);
});
};
o.write = o.remove = function(section_id, value) {
return Promise.all([
firewall.getZoneByNetwork(ifc.getName()),
(value != null) ? firewall.getZone(value) : null
]).then(function(data) {
var old_zone = data[0],
new_zone = data[1];
if (old_zone == null && new_zone == null && (value == null || value == ''))
return;
if (old_zone != null && new_zone != null && old_zone.getName() == new_zone.getName())
return;
if (old_zone != null)
old_zone.deleteNetwork(ifc.getName());
if (new_zone != null)
new_zone.addNetwork(ifc.getName());
else if (value != null)
return firewall.addZone(value).then(function(new_zone) {
new_zone.addNetwork(ifc.getName());
});
});
};
}
for (var i = 0; i < protocols.length; i++) {
proto_select.value(protocols[i].getProtocol(), protocols[i].getI18n());
if (protocols[i].getProtocol() != uci.get('network', s.section, 'proto'))
proto_switch.depends('proto', protocols[i].getProtocol());
}
if (L.hasSystemFeature('dnsmasq') || L.hasSystemFeature('odhcpd')) {
o = s.taboption('dhcp', form.SectionValue, '_dhcp', form.TypedSection, 'dhcp');
ss = o.subsection;
ss.uciconfig = 'dhcp';
ss.addremove = false;
ss.anonymous = true;
ss.tab('general', _('General Setup'));
ss.tab('advanced', _('Advanced Settings'));
ss.tab('ipv6', _('IPv6 Settings'));
ss.tab('ipv6-ra', _('IPv6 RA Settings'));
ss.filter = function(section_id) {
return (uci.get('dhcp', section_id, 'interface') == ifc.getName());
};
ss.renderSectionPlaceholder = function() {
return E('div', { 'class': 'cbi-section-create' }, [
E('p', _('No DHCP Server configured for this interface') + ' '),
E('button', {
'class': 'cbi-button cbi-button-add',
'title': _('Set up DHCP Server'),
'click': ui.createHandlerFn(this, function(section_id, ev) {
this.map.save(function() {
uci.add('dhcp', 'dhcp', section_id);
uci.set('dhcp', section_id, 'interface', section_id);
if (protoval == 'static') {
uci.set('dhcp', section_id, 'start', 100);
uci.set('dhcp', section_id, 'limit', 150);
uci.set('dhcp', section_id, 'leasetime', '12h');
}
else {
uci.set('dhcp', section_id, 'ignore', 1);
}
});
}, ifc.getName())
}, _('Set up DHCP Server'))
]);
};
ss.taboption('general', form.Flag, 'ignore', _('Ignore interface'), _('Disable DHCP for this interface.'));
if (protoval == 'static') {
so = ss.taboption('general', form.Value, 'start', _('Start'), _('Lowest leased address as offset from the network address.'));
so.optional = true;
so.datatype = 'or(uinteger,ip4addr("nomask"))';
so.default = '100';
so = ss.taboption('general', form.Value, 'limit', _('Limit'), _('Maximum number of leased addresses.'));
so.optional = true;
so.datatype = 'uinteger';
so.default = '150';
so = ss.taboption('general', form.Value, 'leasetime', _('Lease time'), _('Expiry time of leased addresses, minimum is 2 minutes (2m
).'));
so.optional = true;
so.default = '12h';
so = ss.taboption('advanced', form.Flag, 'dynamicdhcp', _('Dynamic DHCP'), _('Dynamically allocate DHCP addresses for clients. If disabled, only clients having static leases will be served.'));
so.default = so.enabled;
ss.taboption('advanced', form.Flag, 'force', _('Force'), _('Force DHCP on this network even if another server is detected.'));
// XXX: is this actually useful?
//ss.taboption('advanced', form.Value, 'name', _('Name'), _('Define a name for this network.'));
so = ss.taboption('advanced', form.Value, 'netmask', _('IPv4-Netmask'), _('Override the netmask sent to clients. Normally it is calculated from the subnet that is served.'));
so.optional = true;
so.datatype = 'ip4addr';
so.render = function(option_index, section_id, in_table) {
this.placeholder = get_netmask(s, true);
return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]);
};
so.validate = function(section_id, value) {
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 "6,192.168.2.1,192.168.2.2
" which advertises different DNS servers to clients.'));
}
var has_other_master = uci.sections('dhcp', 'dhcp').filter(function(s) {
return (s.interface != ifc.getName() && s.master == '1');
})[0];
so = ss.taboption('ipv6', form.Flag , 'master', _('Designated master'));
so.readonly = has_other_master ? true : false;
so.description = has_other_master
? _('Interface "%h" is already marked as designated master.').format(has_other_master.interface || has_other_master['.name'])
: _('Set this interface as master for RA and DHCPv6 relaying as well as NDP proxying.')
;
so.validate = function(section_id, value) {
var hybrid_downstream_desc = _('Operate in relay mode if a designated master interface is configured and active, otherwise fall back to server mode.'),
ndp_downstream_desc = _('Operate in relay mode if a designated master interface is configured and active, otherwise disable NDP proxying.'),
hybrid_master_desc = _('Operate in relay mode if an upstream IPv6 prefix is present, otherwise disable service.'),
ra_server_allowed = true,
checked = this.formvalue(section_id),
dhcpv6 = this.section.getOption('dhcpv6').getUIElement(section_id),
ndp = this.section.getOption('ndp').getUIElement(section_id),
ra = this.section.getOption('ra').getUIElement(section_id);
/* Assume that serving RAs by default is fine, but disallow it for certain
interface protocols such as DHCP, DHCPv6 or the various PPP flavors.
The intent is to only allow RA serving for interface protocols doing
some kind of static IP config over something resembling a layer 2
ethernet device. */
switch (protoval) {
case 'dhcp':
case 'dhcpv6':
case '3g':
case 'l2tp':
case 'ppp':
case 'pppoa':
case 'pppoe':
case 'pptp':
case 'pppossh':
case 'ipip':
case 'gre':
case 'grev6':
ra_server_allowed = false;
break;
}
if (checked == '1' || !ra_server_allowed) {
dhcpv6.node.querySelector('li[data-value="server"]').setAttribute('unselectable', '');
if (dhcpv6.getValue() == 'server')
dhcpv6.setValue('hybrid');
ra.node.querySelector('li[data-value="server"]').setAttribute('unselectable', '');
if (ra.getValue() == 'server')
ra.setValue('hybrid');
}
if (checked == '1') {
dhcpv6.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_master_desc;
ra.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_master_desc;
ndp.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_master_desc;
}
else {
if (ra_server_allowed) {
dhcpv6.node.querySelector('li[data-value="server"]').removeAttribute('unselectable');
ra.node.querySelector('li[data-value="server"]').removeAttribute('unselectable');
}
dhcpv6.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_downstream_desc;
ra.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_downstream_desc;
ndp.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = ndp_downstream_desc ;
}
return true;
};
so = ss.taboption('ipv6', form.RichListValue, 'ra', _('RA-Service'),
_('Configures the operation mode of the RA service on this interface.'));
so.optional = true;
so.value('', _('disabled'),
_('Do not send any RA messages on this interface.'));
so.value('server', _('server mode'),
_('Send RA messages advertising this device as IPv6 router.'));
so.value('relay', _('relay mode'),
_('Forward RA messages received on the designated master interface to downstream interfaces.'));
so.value('hybrid', _('hybrid mode'), ' ');
so = ss.taboption('ipv6-ra', form.RichListValue, 'ra_default', _('Default router'),
_('Configures the default router advertisement in RA messages.'));
so.optional = true;
so.value('', _('automatic'),
_('Announce this device as default router if a local IPv6 default route is present.'));
so.value('1', _('on available prefix'),
_('Announce this device as default router if a public IPv6 prefix is available, regardless of local default route availability.'));
so.value('2', _('forced'),
_('Announce this device as default router regardless of whether a prefix or default route is present.'));
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so = ss.taboption('ipv6-ra', form.Flag, 'ra_slaac', _('Enable SLAAC'),
_('Set the autonomous address-configuration flag in the prefix information options of sent RA messages. When enabled, clients will perform stateless IPv6 address autoconfiguration.'));
so.default = so.enabled;
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so = ss.taboption('ipv6-ra', form.RichListValue, 'ra_flags', _('RA Flags'),
_('Specifies the flags sent in RA messages, for example to instruct clients to request further information via stateful DHCPv6.'));
so.optional = true;
so.value('managed-config', _('managed config (M)'),
_('The Managed address configuration (M) flag indicates that IPv6 addresses are available via DHCPv6.'));
so.value('other-config', _('other config (O)'),
_('The Other configuration (O) flag indicates that other information, such as DNS servers, is available via DHCPv6.'));
so.value('home-agent', _('mobile home agent (H)'),
_('The Mobile IPv6 Home Agent (H) flag indicates that the device is also acting as Mobile IPv6 home agent on this link.'));
so.multiple = true;
so.select_placeholder = _('none');
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so.cfgvalue = function(section_id) {
var flags = L.toArray(uci.get('dhcp', section_id, 'ra_flags'));
return flags.length ? flags : [ 'other-config' ];
};
so.remove = function(section_id) {
var existing = L.toArray(uci.get('dhcp', section_id, 'ra_flags'));
if (this.isActive(section_id)) {
if (existing.length != 1 || existing[0] != 'none')
uci.set('dhcp', section_id, 'ra_flags', [ 'none' ]);
}
else if (existing.length) {
uci.unset('dhcp', section_id, 'ra_flags');
}
};
so = ss.taboption('ipv6-ra', form.Value, 'ra_pref64', _('NAT64 prefix'), _('Announce NAT64 prefix in RA messages.'));
so.optional = true;
so.datatype = 'cidr6';
so.placeholder = '64:ff9b::/96';
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so = ss.taboption('ipv6-ra', form.Value, 'ra_maxinterval', _('Max RA interval'), _('Maximum time allowed between sending unsolicited RA. Default is 600 seconds.'));
so.optional = true;
so.datatype = 'uinteger';
so.placeholder = '600';
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so = ss.taboption('ipv6-ra', form.Value, 'ra_mininterval', _('Min RA interval'), _('Minimum time allowed between sending unsolicited RA. Default is 200 seconds.'));
so.optional = true;
so.datatype = 'uinteger';
so.placeholder = '200';
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so = ss.taboption('ipv6-ra', form.Value, 'ra_lifetime', _('RA Lifetime'), _('Router Lifetime published in RA messages. Maximum is 9000 seconds.'));
so.optional = true;
so.datatype = 'range(0, 9000)';
so.placeholder = '1800';
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so = ss.taboption('ipv6-ra', form.Value, 'ra_mtu', _('RA MTU'), _('The MTU to be published in RA messages. Minimum is 1280 bytes.'));
so.optional = true;
so.datatype = 'range(1280, 65535)';
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so.load = function(section_id) {
var dev = ifc.getL3Device(),
path = dev ? "/proc/sys/net/ipv6/conf/%s/mtu".format(dev.getName()) : null;
return Promise.all([
dev ? L.resolveDefault(fs.read(path), dev.getMTU()) : null,
this.super('load', [section_id])
]).then(L.bind(function(res) {
this.placeholder = +res[0];
return res[1];
}, this));
};
so = ss.taboption('ipv6-ra', form.Value, 'ra_hoplimit', _('RA Hop Limit'), _('The maximum hops to be published in RA messages. Maximum is 255 hops.'));
so.optional = true;
so.datatype = 'range(0, 255)';
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so.load = function(section_id) {
var dev = ifc.getL3Device(),
path = dev ? "/proc/sys/net/ipv6/conf/%s/hop_limit".format(dev.getName()) : null;
return Promise.all([
dev ? L.resolveDefault(fs.read(path), 64) : null,
this.super('load', [section_id])
]).then(L.bind(function(res) {
this.placeholder = +res[0];
return res[1];
}, this));
};
so = ss.taboption('ipv6', form.RichListValue, 'dhcpv6', _('DHCPv6-Service'),
_('Configures the operation mode of the DHCPv6 service on this interface.'));
so.optional = true;
so.value('', _('disabled'),
_('Do not offer DHCPv6 service on this interface.'));
so.value('server', _('server mode'),
_('Provide a DHCPv6 server on this interface and reply to DHCPv6 solicitations and requests.'));
so.value('relay', _('relay mode'),
_('Forward DHCPv6 messages between the designated master interface and downstream interfaces.'));
so.value('hybrid', _('hybrid mode'), ' ');
so = ss.taboption('ipv6', form.Value, 'dhcpv6_pd_min_len', _('PD minimum length'),
_('Configures the minimum delegated prefix length assigned to a requesting downstream router, potentially overriding a requested prefix length. If left unspecified, the device will assign the smallest available prefix greater than or equal to the requested prefix.'));
so.datatype = 'range(1,62)';
so.depends('dhcpv6', 'server');
so = ss.taboption('ipv6', form.DynamicList, 'dns', _('Announced IPv6 DNS servers'),
_('Specifies a fixed list of IPv6 DNS server addresses to announce via DHCPv6. If left unspecified, the device will announce itself as IPv6 DNS server unless the Local IPv6 DNS server option is disabled.'));
so.datatype = 'ip6addr("nomask")'; /* restrict to IPv6 only for now since dnsmasq (DHCPv4) does not honour this option */
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so.depends('dhcpv6', 'server');
so.depends({ dhcpv6: 'hybrid', master: '0' });
so = ss.taboption('ipv6', form.Flag, 'dns_service', _('Local IPv6 DNS server'),
_('Announce this device as IPv6 DNS server.'));
so.default = so.enabled;
so.depends({ ra: 'server', dns: /^$/ });
so.depends({ ra: 'hybrid', dns: /^$/, master: '0' });
so.depends({ dhcpv6: 'server', dns: /^$/ });
so.depends({ dhcpv6: 'hybrid', dns: /^$/, master: '0' });
so = ss.taboption('ipv6', form.DynamicList, 'domain', _('Announced DNS domains'),
_('Specifies a fixed list of DNS search domains to announce via DHCPv6. If left unspecified, the local device DNS search domain will be announced.'));
so.datatype = 'hostname';
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so.depends('dhcpv6', 'server');
so.depends({ dhcpv6: 'hybrid', master: '0' });
//This is a DHCPv6 specific odhcpd setting
so = ss.taboption('ipv6', form.DynamicList, 'ntp', _('NTP Servers'),
_('DHCPv6 option 56. %s.', 'DHCPv6 option 56. RFC5908 link').format('RFC5908').format('https://www.rfc-editor.org/rfc/rfc5908#section-4'));
so.datatype = 'host(0)';
for(var x of uci.get('system', 'ntp', 'server') || '') {
so.value(x);
}
var local_nets = this.networks.filter(function(n) { return n.getName() != 'loopback' });
if(local_nets) {
// If ntpd is set up, suggest our IP(v6) also
if(uci.get('system', 'ntp', 'enable_server')) {
local_nets.forEach(function(n){
n.getIPAddrs().forEach(function(i4) {
so.value(i4.split('/')[0]);
});
n.getIP6Addrs().forEach(function(i6) {
so.value(i6.split('/')[0]);
});
});
}
}
so.optional = true;
so.rmempty = true;
so.depends('dhcpv6', 'server');
so.depends({ dhcpv6: 'hybrid', master: '0' });
so = ss.taboption('ipv6', form.RichListValue, 'ndp', _('NDP-Proxy'),
_('Configures the operation mode of the NDP proxy service on this interface.'));
so.optional = true;
so.value('', _('disabled'),
_('Do not proxy any NDP packets.'));
so.value('relay', _('relay mode'),
_('Forward NDP NS and NA messages between the designated master interface and downstream interfaces.'));
so.value('hybrid', _('hybrid mode'), ' ');
so = ss.taboption('ipv6', form.Flag, 'ndproxy_routing', _('Learn routes'), _('Set up routes for proxied IPv6 neighbours.'));
so.default = so.enabled;
so.depends('ndp', 'relay');
so.depends('ndp', 'hybrid');
so = ss.taboption('ipv6', form.Flag, 'ndproxy_slave', _('NDP-Proxy slave'), _('Set interface as NDP-Proxy external slave. Default is off.'));
so.depends({ ndp: 'relay', master: '0' });
so.depends({ ndp: 'hybrid', master: '0' });
so = ss.taboption('ipv6', form.Value, 'preferred_lifetime', _('IPv6 Prefix Lifetime'), _('Preferred lifetime for a prefix.'));
so.optional = true;
so.placeholder = '12h';
so.value('5m', _('5m (5 minutes)'));
so.value('3h', _('3h (3 hours)'));
so.value('12h', _('12h (12 hours - default)'));
so.value('7d', _('7d (7 days)'));
//This is a ra_* setting, but its placement is more logical/findable under IPv6 settings.
so = ss.taboption('ipv6', form.Flag, 'ra_useleasetime', _('Follow IPv4 Lifetime'), _('DHCPv4 leasetime
is used as limit and preferred lifetime of the IPv6 prefix.'));
so.optional = true;
}
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 (has_peerdns(protoval)) {
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 (has_peerdns(protoval))
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'),
_('Metric is an ordinal, where a gateway with 1 is chosen 1st, 2 is chosen 2nd, 3 is chosen 3rd, etc'));
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][1], rtTables[i][0]));
if (has_sourcefilter(protoval)) {
o = nettools.replaceOption(s, 'advanced', form.Flag, 'sourcefilter', _('IPv6 source routing'), _('Automatically handle multiple uplink interfaces using source-based policy routing.'));
o.default = o.enabled;
}
o = nettools.replaceOption(s, 'advanced', form.Flag, 'delegate', _('Delegate IPv6 prefixes'), _('Enable downstream delegation of IPv6 prefixes available on this interface'));
o.default = o.enabled;
o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6assign', _('IPv6 assignment length'), _('Assign a part of given length of every public IPv6-prefix to this interface'));
o.value('', _('disabled'));
o.value('64');
o.datatype = 'max(128)';
o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6hint', _('IPv6 assignment hint'), _('Assign prefix parts using this hexadecimal subprefix ID for this interface.'));
o.placeholder = '0';
o.validate = function(section_id, value) {
if (value == null || value == '')
return true;
var n = parseInt(value, 16);
if (!/^(0x)?[0-9a-fA-F]+$/.test(value) || isNaN(n) || n >= 0xffffffff)
return _('Expecting a hexadecimal assignment hint');
return true;
};
for (var i = 33; i <= 64; i++)
o.depends('ip6assign', String(i));
o = nettools.replaceOption(s, 'advanced', form.DynamicList, 'ip6class', _('IPv6 prefix filter'), _('If set, downstream subnets are only allocated from the given IPv6 prefix classes.'));
o.value('local', 'local (%s)'.format(_('Local ULA')));
var prefixClasses = {};
this.networks.forEach(function(net) {
var prefixes = net._ubus('ipv6-prefix');
if (Array.isArray(prefixes)) {
prefixes.forEach(function(pfx) {
if (L.isObject(pfx) && typeof(pfx['class']) == 'string') {
prefixClasses[pfx['class']] = prefixClasses[pfx['class']] || {};
prefixClasses[pfx['class']][net.getName()] = true;
}
});
}
});
Object.keys(prefixClasses).sort().forEach(function(c) {
var networks = Object.keys(prefixClasses[c]).sort().join(', ');
o.value(c, (c != networks) ? '%s (%s)'.format(c, networks) : c);
});
o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6ifaceid', _('IPv6 suffix'), _("Optional. Allowed values: 'eui64', 'random', fixed value like '::1' or '::1:2'. When IPv6 prefix (like 'a:b:c:d::') is received from a delegating server, use the suffix (like '::1') to form the IPv6 address ('a:b:c:d::1') for the interface."));
o.datatype = 'ip6hostid';
o.placeholder = '::1';
o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6weight', _('IPv6 preference'), _('When delegating prefixes to multiple downstreams, interfaces with a higher preference value are considered first when allocating subnets.'));
o.datatype = 'uinteger';
o.placeholder = '0';
for (var i = 0; i < s.children.length; i++) {
o = s.children[i];
switch (o.option) {
case 'proto':
case 'auto':
case '_dhcp':
case '_zone':
case '_switch_proto':
case '_ifacestat_modal':
continue;
case 'igmp_snooping':
case 'stp':
case 'type':
case '_net_device':
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.deps[j].proto = protoval;
else
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, device;
protocols.sort(function(a, b) {
return L.naturalCompare(a.getProtocol(), b.getProtocol());
});
s2.render = function() {
return Promise.all([
{},
this.renderUCISection('_new_')
]).then(this.renderContents.bind(this));
};
name = s2.option(form.Value, 'name', _('Name'));
name.rmempty = false;
name.datatype = 'uciname';
name.placeholder = _('New interface name…');
name.validate = function(section_id, value) {
if (uci.get('network', value) != null)
return _('The interface name is already used');
var pr = network.getProtocol(proto.formvalue(section_id), value),
ifname = pr.isVirtual() ? '%s-%s'.format(pr.getProtocol(), value) : 'br-%s'.format(value);
if (value.length > 15)
return _('The interface name is too long');
return true;
};
proto = s2.option(form.ListValue, 'proto', _('Protocol'));
proto.onchange = function(ev, section_id, value) {
var elem = name.getUIElement(section_id);
elem.triggerValidation();
};
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())
device.depends('proto', protocols[i].getProtocol());
}
m2.render().then(L.bind(function(nodes) {
ui.showModal(_('Add new interface...'), [
nodes,
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Cancel')), ' ',
E('button', {
'class': 'cbi-button cbi-button-positive important',
'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, nameval) : null;
if (nameval == null || protoval == null || nameval == '' || protoval == '')
return;
return protoclass.isCreateable(nameval).then(function(checkval) {
if (checkval != null) {
ui.addNotification(null,
E('p', _('New interface for "%s" can not be created: %s').format(protoclass.getI18n(), checkval)));
ui.hideModal();
return;
}
return m.save(function() {
var section_id = uci.add('network', 'interface', nameval);
protoclass.set('proto', protoval);
protoclass.addDevice(device.formvalue('_new_'));
m.children[0].addedSection = section_id;
ui.hideModal();
ui.showModal(null, E('p', { 'class': 'spinning' }, [ _('Loading data…') ]));
}).then(L.bind(m.children[0].renderMoreOptionsModal, m.children[0], nameval));
});
})
}, _('Create interface'))
])
], 'cbi-modal');
nodes.querySelector('[id="%s"] input[type="text"]'.format(name.cbid('_new_'))).focus();
}, this));
};
s.handleRemove = function(section_id, ev) {
return network.deleteNetwork(section_id).then(L.bind(function(section_id, ev) {
return form.GridSection.prototype.handleRemove.apply(this, [section_id, ev]);
}, this, section_id, ev));
};
o = s.option(form.DummyValue, '_ifacebox');
o.modalonly = false;
o.textvalue = function(section_id) {
var net = this.section.networks.filter(function(n) { return n.getName() == section_id })[0],
zone = net ? this.section.zones.filter(function(z) { return !!z.getNetworks().filter(function(n) { return n == section_id })[0] })[0] : null;
if (!net)
return;
var node = E('div', { 'class': 'ifacebox' }, [
E('div', {
'class': 'ifacebox-head',
'style': firewall.getZoneColorStyle(zone),
'title': zone ? _('Part of zone %q').format(zone.getName()) : _('No zone assigned')
}, E('strong', net.getName())),
E('div', {
'class': 'ifacebox-body',
'id': '%s-ifc-devices'.format(section_id),
'data-network': section_id
}, [
E('img', {
'src': L.resource('icons/ethernet_disabled.png'),
'style': 'width:16px; height:16px'
}),
E('br'), E('small', '?')
])
]);
render_ifacebox_status(node.childNodes[1], net);
return node;
};
o = s.option(form.DummyValue, '_ifacestat');
o.modalonly = false;
o.textvalue = function(section_id) {
var net = this.section.networks.filter(function(n) { return n.getName() == section_id })[0];
if (!net)
return;
var node = E('div', { 'id': '%s-ifc-description'.format(section_id) });
render_status(node, net, false);
return node;
};
o = s.taboption('advanced', form.Flag, 'delegate', _('Use builtin IPv6-management'));
o.modalonly = true;
o.default = o.enabled;
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.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 L.naturalCompare(a.name, b.name) }).map(function(s) { return s['.name'] });
for (var i = 0; i < netDevs.length; i++) {
if (sections.filter(function(s) { return s.name == netDevs[i].getName() }).length)
continue;
if (netDevs[i].getType() == 'wifi' && !netDevs[i].isUp())
continue;
/* Unless http://lists.openwrt.org/pipermail/openwrt-devel/2020-July/030397.html is implemented,
we cannot properly redefine bridges as devices, so filter them away for now... */
var m = netDevs[i].isBridge() ? netDevs[i].getName().match(/^br-([A-Za-z0-9_]+)$/) : null,
s = m ? uci.get('network', m[1]) : null;
if (s && s['.type'] == 'interface' && s.type == 'bridge')
continue;
section_ids.push('dev:%s'.format(netDevs[i].getName()));
}
return section_ids;
};
s.renderMoreOptionsModal = function(section_id, ev) {
var m = section_id.match(/^dev:(.+)$/);
if (m) {
var devtype = getDevType(section_id);
section_id = uci.add('network', 'device');
uci.set('network', section_id, 'name', m[1]);
uci.set('network', section_id, 'type', (devtype != 'ethernet') ? devtype : null);
this.addedSection = section_id;
}
return this.super('renderMoreOptionsModal', [section_id, ev]);
};
s.renderRowActions = function(section_id) {
var trEl = this.super('renderRowActions', [ section_id, _('Configure…') ]),
deleteBtn = trEl.querySelector('button:last-child');
deleteBtn.firstChild.data = _('Unconfigure');
deleteBtn.setAttribute('title', _('Remove related device settings from the configuration'));
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(map /*, ... */) {
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']);
});
if (map.addedVLANs)
for (var i = 0; i < map.addedVLANs.length; i++)
uci.remove('network', map.addedVLANs[i]);
if (this.addedSection)
uci.remove('network', this.addedSection);
return form.GridSection.prototype.handleModalCancel.apply(this, arguments);
};
s.handleRemove = function(section_id /*, ... */) {
var name = uci.get('network', section_id, 'name'),
type = uci.get('network', section_id, 'type');
if (name != null && type == 'bridge') {
uci.sections('network', 'bridge-vlan', function(bvs) {
if (bvs.device == name)
uci.remove('network', bvs['.name']);
});
}
return form.GridSection.prototype.handleRemove.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 dev = getDevice(section_id),
cfg = uci.get('network', section_id),
type = cfg ? (uci.get('network', section_id, 'type') || 'ethernet') : (dev ? dev.getType() : '');
switch (type) {
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.')
}, [ 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.')
}, [ 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'),
_('Unique Local Address (%s) - prefix fd00::/8
(the L bit is always 1).').format('RFC4193').format('https://datatracker.ietf.org/doc/html/rfc4193#section-3') + ' ' +
_('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.ListValue, 'packet_steering', _('Packet Steering'), _('Enable packet steering across CPUs. May help or hinder network speed.'));
o.value('', _('Disabled'));
o.value('1',_('Enabled'));
o.value('2',_('Enabled (all CPUs)'));
o.optional = true;
var steer_flow = uci.get('network', 'globals', 'steering_flows');
o = s.option(form.Value, 'steering_flows', _('Steering flows (RPS)'),
_('Directs packet flows to specific CPUs where the local socket owner listens (the local service).') + ' ' +
_('Note: this setting is for local services on the device only (not for forwarding).'));
o.value('', _('Standard: none'));
o.value('128', _('Suggested: 128'));
o.value('256', _('256'));
o.depends('packet_steering', '1');
o.depends('packet_steering', '2');
o.datatype = 'uinteger';
o.default = steer_flow;
if (dslModemType != null) {
s = m.section(form.TypedSection, 'dsl', _('DSL'));
s.anonymous = true;
o = s.option(form.ListValue, 'annex', _('Annex'));
if (dslModemType == 'vdsl') {
o.value('a', _('ADSL (all variants) Annex A/L/M + VDSL2 Annex A/B/C'));
o.value('b', _('ADSL (all variants) Annex B + VDSL2 Annex A/B/C'));
o.value('j', _('ADSL (all variants) Annex B/J + VDSL2 Annex A/B/C'));
} else {
o.value('a', _('ADSL (all variants) Annex A/L/M'));
o.value('b', _('ADSL (all variants) Annex B'));
o.value('j', _('ADSL (all variants) Annex B/J'));
}
o.value('m', _('ADSL (all variants) Annex M'));
o.value('at1', _('ANSI T1.413'));
o.value('admt', _('ADSL (G.992.1) Annex A'));
o.value('bdmt', _('ADSL (G.992.1) Annex B'));
o.value('alite', _('Splitterless ADSL (G.992.2) Annex A'));
o.value('a2', _('ADSL2 (G.992.3) Annex A'));
o.value('b2', _('ADSL2 (G.992.3) Annex B'));
o.value('l', _('ADSL2 (G.992.3) Annex L'));
o.value('m2', _('ADSL2 (G.992.3) Annex M'));
o.value('a2p', _('ADSL2+ (G.992.5) Annex A'));
o.value('b2p', _('ADSL2+ (G.992.5) Annex B'));
o.value('m2p', _('ADSL2+ (G.992.5) Annex M'));
o = s.option(form.ListValue, 'tone', _('Tone'));
o.value('', _('auto'));
o.value('a', _('A43C + J43 + A43'));
o.value('av', _('A43C + J43 + A43 + V43'));
o.value('b', _('B43 + B43C'));
o.value('bv', _('B43 + B43C + V43'));
if (dslModemType == 'vdsl') {
o = s.option(form.ListValue, 'xfer_mode', _('Encapsulation mode'));
o.value('', _('auto'));
o.value('atm', _('ATM (Asynchronous Transfer Mode)'));
o.value('ptm', _('PTM/EFM (Packet Transfer Mode)'));
o = s.option(form.ListValue, 'line_mode', _('DSL line mode'));
o.value('', _('auto'));
o.value('adsl', _('ADSL'));
o.value('vdsl', _('VDSL'));
o = s.option(form.ListValue, 'ds_snr_offset', _('Downstream SNR offset'));
o.default = '0';
for (var i = -100; i <= 100; i += 5)
o.value(i, _('%.1f dB').format(i / 10));
}
s.option(form.Value, 'firmware', _('Firmware File'));
}
// Show ATM bridge section if we have the capabilities
if (L.hasSystemFeature('br2684ctl')) {
s = m.section(form.TypedSection, 'atm-bridge', _('ATM Bridges'), _('ATM bridges expose encapsulated ethernet in AAL5 connections as virtual Linux network interfaces which can be used in conjunction with DHCP or PPP to dial into the provider network.'));
s.addremove = true;
s.anonymous = true;
s.addbtntitle = _('Add ATM Bridge');
s.handleAdd = function(ev) {
var sections = uci.sections('network', 'atm-bridge'),
max_unit = -1;
for (var i = 0; i < sections.length; i++) {
var unit = +sections[i].unit;
if (!isNaN(unit) && unit > max_unit)
max_unit = unit;
}
return this.map.save(function() {
var sid = uci.add('network', 'atm-bridge');
uci.set('network', sid, 'unit', max_unit + 1);
uci.set('network', sid, 'atmdev', 0);
uci.set('network', sid, 'encaps', 'llc');
uci.set('network', sid, 'payload', 'bridged');
uci.set('network', sid, 'vci', 35);
uci.set('network', sid, 'vpi', 8);
});
};
s.tab('general', _('General Setup'));
s.tab('advanced', _('Advanced Settings'));
o = s.taboption('general', form.Value, 'vci', _('ATM Virtual Channel Identifier (VCI)'));
s.taboption('general', form.Value, 'vpi', _('ATM Virtual Path Identifier (VPI)'));
o = s.taboption('general', form.ListValue, 'encaps', _('Encapsulation mode'));
o.value('llc', _('LLC'));
o.value('vc', _('VC-Mux'));
s.taboption('advanced', form.Value, 'atmdev', _('ATM device number'));
s.taboption('advanced', form.Value, 'unit', _('Bridge unit number'));
o = s.taboption('advanced', form.ListValue, 'payload', _('Forwarding mode'));
o.value('bridged', _('bridged'));
o.value('routed', _('routed'));
}
return m.render().then(L.bind(function(m, nodes) {
poll.add(L.bind(function() {
var section_ids = m.children[0].cfgsections(),
tasks = [];
for (var i = 0; i < section_ids.length; i++) {
var row = nodes.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_ids[i])),
dsc = row.querySelector('[data-name="_ifacestat"] > div'),
btn1 = row.querySelector('.cbi-section-actions .reconnect'),
btn2 = row.querySelector('.cbi-section-actions .down');
if (dsc.getAttribute('reconnect') == '') {
dsc.setAttribute('reconnect', '1');
tasks.push(fs.exec('/sbin/ifup', [section_ids[i]]).catch(function(e) {
ui.addNotification(null, E('p', e.message));
}));
}
else if (dsc.getAttribute('disconnect') == '') {
dsc.setAttribute('disconnect', '1');
tasks.push(fs.exec('/sbin/ifdown', [section_ids[i]]).catch(function(e) {
ui.addNotification(null, E('p', e.message));
}));
}
else if (dsc.getAttribute('reconnect') == '1') {
dsc.removeAttribute('reconnect');
btn1.classList.remove('spinning');
btn1.disabled = false;
}
else if (dsc.getAttribute('disconnect') == '1') {
dsc.removeAttribute('disconnect');
btn2.classList.remove('spinning');
btn2.disabled = false;
}
}
return Promise.all(tasks)
.then(L.bind(network.getNetworks, network))
.then(L.bind(this.poll_status, this, nodes));
}, this), 5);
return nodes;
}, this, m));
}
});