'use strict';
'require view';
'require dom';
'require poll';
'require fs';
'require ui';
'require uci';
'require rpc';
'require form';
'require network';
'require firewall';
'require tools.widgets as widgets';
'require tools.network as nettools';
const callNetworkDeviceStatus = rpc.declare({
object: 'network.device',
method: 'status',
params: [ 'name' ],
expect: { '': {} }
});
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 (let c of changes.network)
n += (c[1] == section_id);
if (Array.isArray(changes.dhcp))
for (let c of changes.dhcp)
n += (c[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.svg').format(
alias ? 'alias' : type,
up ? '' : '_disabled') }),
E('span', { 'class': 'cbi-tooltip ifacebadge large' }, [
E('img', { 'src': L.resource('icons/%s%s.svg').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) {
let desc = null;
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 carrier = maindev ? maindev.getCarrier(): null;
const cond00 = !changecount && !ifc.isDynamic() && !ifc.isAlias();
const cond01 = cond00 && macaddr;
const cond02 = cond00 && maindev;
const cond03 = cond00 && carrier;
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,
_('Carrier'), (cond03) ? _('Present') : _('Absent'),
_('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) {
// order is important: ifc.getL3Device() can determine dev.getType for tunnel configs
const dev = ifc ? (ifc.getL3Device() ?? ifc.getDevice()) : null;
dom.content(node, [
E('img', {
'src': L.resource('icons/%s%s.svg').format(dev ? dev.getType() : 'ethernet', ifc.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) {
const dev = ifc.getL3Device() || ifc.getDevice();
const subdevs = dev ? dev.getPorts() : null;
const 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) {
const row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(id));
const dsc = row.querySelector('[data-name="_ifacestat"] > div');
const 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) {
const readfn = use_cfgvalue ? 'cfgvalue' : 'formvalue';
const addrs = L.toArray(s[readfn](s.section, 'ipaddr'));
const mask = s[readfn](s.section, 'netmask');
const firstsubnet = mask ? addrs[0] + '/' + mask : addrs.filter(function(a) { return a.indexOf('/') > 0 })[0];
if (firstsubnet == null)
return null;
let 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(map, networks) {
let resolveZone = null;
for (let ifc of networks) {
const row = map.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(ifc.getName()));
if (row == null)
continue;
const dsc = row.querySelector('[data-name="_ifacestat"] > div');
const box = row.querySelector('[data-name="_ifacebox"] .ifacebox-body');
const btn1 = row.querySelector('.cbi-section-actions .reconnect');
const btn2 = row.querySelector('.cbi-section-actions .down');
const stat = document.querySelector('[id="%s-ifc-status"]'.format(ifc.getName()));
resolveZone = render_ifacebox_status(box, ifc);
const disabled = ifc ? !ifc.isUp() : true;
const dynamic = ifc ? ifc.isDynamic() : false;
const pending = ifc ? ifc.isPending() : 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()) {
const e = map.querySelector('[id="cbi-network-%s"] .cbi-button-edit'.format(ifc.getName()));
if (e) e.disabled = true;
const link = L.url('admin/system/package-manager') + '?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) {
const dev = ifc.getL3Device() ?? ifc.getDevice();
dom.content(stat, [
E('img', {
'src': L.resource('icons/%s%s.svg').format(dev ? dev.getType() : 'ethernet', ifc.isUp() ? '' : '_disabled'),
'title': dev ? dev.getTypeI18n() : _('Not present')
}),
render_status(E('span'), ifc, true)
]);
}
if (isReadonlyView === true) {
btn1.disabled = true;
btn2.disabled = true;
}
else if (btn1.classList.contains('spinning') || btn2.classList.contains('spinning')) {
btn1.disabled = true;
btn2.disabled = true;
}
else if (dynamic === true) {
btn1.disabled = true;
btn2.disabled = true;
}
else if (pending === true) {
btn1.disabled = true;
btn2.disabled = false;
}
else if (disabled === true) {
btn1.disabled = false;
btn2.disabled = true;
}
else {
btn1.disabled = false;
btn2.disabled = false;
}
}
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() {
return Promise.all([
network.getDSLModemType(),
network.getDevices(),
fs.lines('/etc/iproute2/rt_tables'),
uci.changes()
]);
},
interfaceBridgeWithIfnameSections() {
return uci.sections('network', 'interface').filter(function(ns) {
return ns.type == 'bridge' && !ns.ports && ns.ifname;
});
},
deviceWithIfnameSections() {
return uci.sections('network', 'device').filter(function(ns) {
return ns.type == 'bridge' && !ns.ports && ns.ifname;
});
},
interfaceWithIfnameSections() {
return uci.sections('network', 'interface').filter(function(ns) {
return !ns.device && ns.ifname;
});
},
handleBridgeMigration(ev) {
const tasks = [];
this.interfaceBridgeWithIfnameSections().forEach(function(ns) {
const 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() {
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(ev) {
const 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() {
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([dslModemType, netDevs, rtTables]) {
if (this.interfaceBridgeWithIfnameSections().length)
return this.renderBridgeMigration();
else if (this.deviceWithIfnameSections().length || this.interfaceWithIfnameSections().length)
return this.renderIfnameMigration();
let m, s, o;
rtTables = rtTables.map(function(l) {
const 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.filterrow = 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,
pending = net ? net.isPending() : 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'),
}, _('Restart')),
E('button', {
'class': 'cbi-button cbi-button-neutral down',
'click': iface_updown.bind(this, false, section_id),
'title': _('Shutdown this interface'),
}, _('Stop')),
tdEl.lastChild.firstChild,
tdEl.lastChild.lastChild
]);
if (!dynamic && net && !uci.get('network', net.getName())) {
tdEl.lastChild.childNodes[0].disabled = true;
tdEl.lastChild.childNodes[1].disabled = true;
tdEl.lastChild.childNodes[2].disabled = true;
tdEl.lastChild.childNodes[3].disabled = true;
}
else if(dynamic === true) {
tdEl.lastChild.childNodes[0].disabled = true;
tdEl.lastChild.childNodes[1].disabled = true;
tdEl.lastChild.childNodes[2].disabled = true;
tdEl.lastChild.childNodes[3].disabled = true;
}
else if(pending === true) {
tdEl.lastChild.childNodes[0].disabled = true;
tdEl.lastChild.childNodes[1].disabled = false;
tdEl.lastChild.childNodes[2].disabled = false;
tdEl.lastChild.childNodes[3].disabled = false;
}
else if (disabled === true){
tdEl.lastChild.childNodes[0].disabled = false;
tdEl.lastChild.childNodes[1].disabled = true;
tdEl.lastChild.childNodes[2].disabled = false;
tdEl.lastChild.childNodes[3].disabled = false;
}
else {
tdEl.lastChild.childNodes[0].disabled = false;
tdEl.lastChild.childNodes[1].disabled = false;
tdEl.lastChild.childNodes[2].disabled = false;
tdEl.lastChild.childNodes[3].disabled = false;
}
return tdEl;
};
s.addModalOptions = function(s) {
const protoval = uci.get('network', s.section, 'proto') || 'none';
let o, proto_select, proto_switch, ss, so;
return network.getNetwork(s.section).then(L.bind(function(ifc) {
const 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) {
const 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_select.default = 'none';
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([old_zone, new_zone]) {
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 (let p of protocols) {
proto_select.value(p.getProtocol(), p.getI18n());
if (p.getProtocol() != protoval)
proto_switch.depends('proto', p.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('ipv4', _('IPv4 Settings'));
ss.tab('ipv6', _('IPv6 Settings'));
ss.tab('dhcpv6', _('DHCPv6 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');
uci.set('dhcp', section_id, 'dhcpv4', 'server');
} else {
uci.set('dhcp', section_id, 'ignore', 1);
}
});
}, ifc.getName())
}, _('Set up DHCP Server'))
]);
};
if (L.hasSystemFeature('dnsmasq')) {
ss.taboption('general', form.Flag, 'ignore', _('Ignore interface'),
_('Disable DHCP for this interface (dnsmasq only).'));
}
if (protoval == 'static') {
so = ss.taboption('general', 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;
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.validate = function (section_id, value) {
if (value === "infinite" || value === "deprecated") {
return true;
}
const regex = new RegExp("^[0-9]+[smhdw]?$", "i");
if (regex.test(value)) {
return true;
}
return _("Invalid DHCP lease time format. Use integer values optionally followed by s, m, h, d, or w.");
}
if (L.hasSystemFeature('dnsmasq')) {
ss.taboption('general', form.Flag, 'force', _('Force'),
_('Force DHCP on this network even if another server is detected (dnsmasq only).'));
}
if (L.hasSystemFeature('dnsmasq')) {
ss.taboption('general', 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 (dnsmasq only).'));
ss.taboption('general', form.DynamicList, 'dhcp_option_force', _('Force DHCP-Options'),
_('As DHCP-Options; send unsolicited (dnsmasq only).'));
}
if (L.hasSystemFeature('odhcpd', 'dhcpv4') || L.hasSystemFeature('dnsmasq')) {
so = ss.taboption('ipv4', form.RichListValue, 'dhcpv4', _('DHCPv4 Service'),
_('Enable or disable DHCPv4 services on this interface.'));
so.optional = true;
so.value('disabled', _('disabled'),
_('Do not provide DHCPv4 services on this interface.'));
so.value('server', _('enabled'),
_('Provide DHCPv4 services on this interface.'));
}
if (L.hasSystemFeature('odhcpd', 'dhcpv4')) {
so = ss.taboption('ipv4', form.Value, 'ipv6_only_preferred', _('IPv6-Only Preferred'),
_('Specifies how often (in seconds) clients should check whether IPv6-only mode is still preferred (see %s, odhcpd only).')
.format('RFC8925').format('https://www.rfc-editor.org/rfc/rfc8925'));
so.optional = true;
so.datatype = 'or(0, range(300,86400))';
so.default = '0';
}
so = ss.taboption('ipv4', form.Value, 'start', _('Start', 'DHCP IP range start address'), _('Lowest leased address as offset from the network address.'));
so.optional = true;
so.datatype = 'or(uinteger,ip4addr("nomask"))';
so.default = '100';
so = ss.taboption('ipv4', form.Value, 'limit', _('Limit'), _('Maximum number of leased addresses.'));
so.optional = true;
so.datatype = 'uinteger';
so.default = '150';
if (L.hasSystemFeature('dnsmasq')) {
so = ss.taboption('ipv4', form.Value, 'netmask', _('IPv4-Netmask'),
_('Override the netmask sent to clients. Normally it is calculated from the subnet that is served (dnsmasq only).'));
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) {
const uielem = this.getUIElement(section_id);
if (uielem)
uielem.setPlaceholder(get_netmask(s, false));
return form.Value.prototype.validate.apply(this, [ section_id, value ]);
};
}
}
const 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) {
const hybrid_downstream_desc = _('Operate in relay mode if a designated master interface is configured and active, otherwise fall back to server mode.');
const ndp_downstream_desc = _('Operate in relay mode if a designated master interface is configured and active, otherwise disable NDP proxying.');
const hybrid_master_desc = _('Operate in relay mode if an upstream IPv6 prefix is present, otherwise disable service.');
let ra_server_allowed = true;
const checked = this.formvalue(section_id);
const dhcpv6 = this.section.getOption('dhcpv6').getUIElement(section_id);
const ndp = this.section.getOption('ndp').getUIElement(section_id);
const 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 only to 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.ListValue, 'ra_preference', _('Router Priority'),
_('A tie-breaker for clients and their routes when multiple routers exist on the same network.'));
so.default = 'medium';
so.value('low', _('Low'));
so.value('medium', _('Medium'));
so.value('high', _('High'));
so.depends('ra', 'server');
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) {
const flags = L.toArray(uci.get('dhcp', section_id, 'ra_flags'));
return flags.length ? flags : [ 'other-config' ];
};
so.remove = function(section_id) {
const 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.RichListValue, '_ra_pio_flags', _('RA PIO Flags'),
_('Allow PIO flags sent in RA messages, globally for all prefixes.'));
so.optional = true;
so.value('pd', _('prefix delegation (PD) preferred (P)'),
_('The PD preferred (P) flag indicates that the network prefers clients use PD instead of individual addresses via DHCPv6 or SLAAC. Requires DHCPv6 and DHCPv6-PD.'));
/* Here PD-P depends on RA being in server or hybrid mode to
distribute the PIO P flag.
For odhcpd to send the PIO P flag set to on requires:
- dhcpv6 on [server, hybrid]
- dhcpv6_pd on
- dhcpv6_pd_preferred on
For odhcpd to relay an upstream PIO P flag which is on requires:
- dhcpv6 on [relay]
- dhcpv6_pd on
- dhcpv6_pd_preferred on
otherwise a relayed P flag is disabled.
If a user runs a separate DHCPv6 server (i.e. not odhcpd), the
named settings must still be on to enable the RA PIO P flag here,
since that is what odhcpd expects.
*/
so.multiple = true;
so.default = '';
so.select_placeholder = _('none');
so.depends({ ra: 'server' });
so.depends({ ra: 'hybrid', master: '0' });
so.cfgvalue = function(section_id) {
const p_flag = uci.get('dhcp', section_id, 'dhcpv6_pd_preferred');
const flags = [];
if (p_flag) flags.push('pd');
return flags;
};
so.remove = function(section_id) {
uci.unset('dhcp', section_id, 'dhcpv6_pd_preferred');
};
so.write = function(section_id, value) {
if (value?.includes('pd'))
uci.set('dhcp', section_id, 'dhcpv6_pd_preferred', '1');
return;
}
so = ss.taboption('ipv6-ra', form.Value, 'ra_pref64', _('NAT64 prefix'), _('Announce NAT64 prefix in RA messages.') + ' ' +
_('See %s and %s.').format('RFC6146', 'RFC8781').format('https://www.rfc-editor.org/rfc/rfc6146', 'https://www.rfc-editor.org/rfc/rfc8781'));
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_reachabletime', _('RA Reachability Timer'),
_('Units: milliseconds. 0 means unspecified.') + ' ' +
_('Dictates how long a node assumes a neighbor is reachable after a reachability confirmation; published in RA messages.'));
so.optional = true;
so.datatype = 'range(0, 3600000)'; // RFC4861 and odhcpd caps to 3,600,000 msec
so.placeholder = '0';
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so = ss.taboption('ipv6-ra', form.Value, 'ra_retranstime', _('RA Retransmission Timer'),
_('Units: milliseconds. 0 means unspecified.') + ' ' +
_('Controls retransmitted Neighbor Solicitation messages; published in RA messages.'));
so.optional = true;
so.placeholder = '0';
so.datatype = 'range(0, 60000)'; // odhcpd caps to 60,000 msec
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 = '2700';
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) {
const dev = ifc.getL3Device();
const 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) {
const dev = ifc.getL3Device();
const 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-ra', form.Value, 'max_preferred_lifetime', _('IPv6 Preferred Prefix Lifetime'), _('Maximum preferred lifetime for a prefix.'));
so.optional = true;
so.placeholder = '45m';
so.value('5m', _('5m (5 minutes)'));
so.value('45m', _('45m (45 minutes - default)'));
so.value('3h', _('3h (3 hours)'));
so.value('12h', _('12h (12 hours)'));
so.value('7d', _('7d (7 days)'));
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
so = ss.taboption('ipv6-ra', form.Value, 'max_valid_lifetime', _('IPv6 Valid Prefix Lifetime'), _('Maximum valid lifetime for a prefix.'));
so.optional = true;
so.placeholder = '90m';
so.value('5m', _('5m (5 minutes)'));
so.value('90m', _('90m (90 minutes - default)'));
so.value('3h', _('3h (3 hours)'));
so.value('12h', _('12h (12 hours)'));
so.value('7d', _('7d (7 days)'));
so.depends('ra', 'server');
so.depends({ ra: 'hybrid', master: '0' });
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('dhcpv6', form.Flag, 'dhcpv6_pd', _('DHCPv6-PD'),
_('Toggle IPv6 PD via DHCPv6.'));
so.depends({ dhcpv6: 'server' });
so = ss.taboption('dhcpv6', 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.placeholder = '62';
so.datatype = 'range(1,64)';
so.depends({ dhcpv6: 'server', dhcpv6_pd: '1' });
/* This option is used by odhcpd. It can take IPv4/6 entries, although IPv4 DNS servers don't
always make sense in an IPv6 environment, they might in a dual stack environment. */
so = ss.taboption('ipv6', form.DynamicList, 'dns', _('Announce IPv4/6 DNS servers'),
_('Specifies a fixed list of DNS server addresses to announce via DHCPv6.') + '
' +
_('Specifies a fixed list of DNS server addresses to announce via RA.') + '
' +
_('If left unspecified, the device will announce itself as DNS server unless the Local IPv6 DNS server option is disabled.'));
so.datatype = 'ipaddr("nomask")';
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, 'dnr', _('Announce encrypted DNS servers'),
_('Specifies a fixed list of encrypted DNS server addresses to announce via DHCPv6/RA (see %s).')
.format('RFC9463').format('https://www.rfc-editor.org/rfc/rfc9463') + '
' +
_('IPv4 addresses are only supported if odhcpd also handles DHCPv4.') + '
' +
_('Syntax: <numeric priority> <domain-name> [IP,...] [SVC parameter ...]') + '
' +
_('Example: 100 dns.example.com 2001:db8::53,192.168.1.53 alpn=doq port=853') + '
' +
_('Note: the _lifetime=<seconds> SVC parameter sets the lifetime of the announced server (use 0 to indicate a server which should no longer be used).')
);
so.datatype = 'string';
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.DynamicList, 'domain', _('Announce DNS domains'),
_('Specifies a fixed list of DNS search domains to announce via DHCPv6.'));
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('dhcpv6', 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(let x of uci.get('system', 'ntp', 'server') || '') {
so.value(x);
}
const 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' });
}
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.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.RichListValue, 'multipath', _('Multi-Path TCP'),
_('Multi-Path TCP') + ' %s'.format('RFC8684').format('https://www.rfc-editor.org/rfc/rfc8684.html') + '
' +
_('For packets originating from this device, e.g. VPN.') );
o.value('off', _('Off'), _('Disables this interface for MPTCP'));
o.value('on', _('On'), _('No special configuration'));
o.value('master', _('Master'), _('Sets default route for all traffic'));
o.value('backup', _('Backup'), _('Hot standby; use this interface; do not forward traffic until no other interface is available (faster)'));
o.value('handover', _('Handover'), _('Cold standby; Establish a connection only when no other interface is available (slower)'));
o = nettools.replaceOption(s, 'advanced', form.Value, 'ip4table', _('Override IPv4 routing table'));
o.datatype = 'or(uinteger, string)';
for (let rt of rtTables)
o.value(rt[1], '%s (%d)'.format(rt[1], rt[0]));
o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6table', _('Override IPv6 routing table'));
o.datatype = 'or(uinteger, string)';
for (let rt of rtTables)
o.value(rt[1], '%s (%d)'.format(rt[1], rt[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;
const 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 (let 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')));
const prefixClasses = {};
this.networks.forEach(function(net) {
const 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) {
const 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 (let o of s.children) {
const deps = [];
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':
for (let p of protocols) {
if (!p.isVirtual()) {
if (o.deps.length)
for (let od of o.deps)
deps.push(Object.assign({ proto: p.getProtocol() }, od));
else
deps.push({ proto: p.getProtocol() });
}
}
o.deps = deps;
break;
default:
if (o.deps.length)
for (let od of o.deps)
od.proto = protoval;
else
o.depends('proto', protoval);
}
}
this.activeSection = s.section;
}, this));
};
s.handleModalCancel = function(/* ... */) {
const type = uci.get('network', this.activeSection || this.addedSection, 'type');
const 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) {
const m2 = new form.Map('network');
const s2 = m2.section(form.NamedSection, '_new_');
const protocols = network.getProtocols();
let 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');
const pr = network.getProtocol(proto.formvalue(section_id), value);
const ifname = pr.isVirtual() ? '%s-%s'.format(pr.getProtocol(), value) : 'br-%s'.format(value);
if (ifname.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) {
const elem = name.getUIElement(section_id);
elem.triggerValidation();
};
device = s2.option(widgets.DeviceSelect, 'device', _('Device'));
device.noaliases = false;
device.optional = false;
for (let p of protocols) {
proto.value(p.getProtocol(), p.getI18n());
if (!p.isVirtual())
device.depends('proto', p.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) {
const nameval = name.isValid('_new_') ? name.formvalue('_new_') : null;
const protoval = proto.isValid('_new_') ? proto.formvalue('_new_') : null;
const 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() {
const 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) {
const net = this.section.networks.filter(function(n) { return n.getName() == section_id })[0];
const zone = net ? this.section.zones.filter(function(z) { return !!z.getNetworks().filter(function(n) { return n == section_id })[0] })[0] : null;
if (!net)
return;
const 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.svg'),
'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) {
const net = this.section.networks.filter(function(n) { return n.getName() == section_id })[0];
if (!net)
return;
const 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() {
const sections = uci.sections('network', 'device');
const section_ids = sections.sort(function(a, b) { return L.naturalCompare(a.name, b.name) }).map(function(s) { return s['.name'] });
for (let nd of netDevs) {
if (sections.filter(function(s) { return s.name == nd.getName() }).length)
continue;
if (nd.getType() == 'wifi' && !nd.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... */
const m = nd.isBridge() ? nd.getName().match(/^br-([A-Za-z0-9_]+)$/) : null;
const s = m ? uci.get('network', m[1]) : null;
if (s && s['.type'] == 'interface' && s.type == 'bridge')
continue;
section_ids.push('dev:%s'.format(nd.getName()));
}
return section_ids;
};
s.renderMoreOptionsModal = function(section_id, ev) {
const m = section_id.match(/^dev:(.+)$/);
if (m) {
const 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) {
const trEl = this.super('renderRowActions', [ section_id, _('Configure…') ]);
const 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) {
const m = section_id.match(/^dev:(.+)$/);
const 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) {
const isNew = (uci.get('network', s.section, 'name') == null);
const dev = getDevice(s.section);
const devName = dev ? dev.getName() : null;
/* Query PSE status from netifd to determine if device has PSE capability */
if (devName) {
return L.resolveDefault(callNetworkDeviceStatus(devName), {}).then((status) => {
const hasPSE = (status.pse != null);
nettools.addDeviceOptions(s, dev, isNew, rtTables, hasPSE);
});
} else {
nettools.addDeviceOptions(s, dev, isNew, rtTables, false);
return Promise.resolve();
}
};
s.handleModalCancel = function(map /*, ... */) {
const 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 (let mav of map.addedVLANs)
uci.remove('network', mav);
if (this.addedSection)
uci.remove('network', this.addedSection);
return form.GridSection.prototype.handleModalCancel.apply(this, arguments);
};
s.handleRemove = function(section_id /*, ... */) {
const name = uci.get('network', section_id, 'name');
const 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) {
const m = section_id.match(/^dev:(.+)$/);
const name = m ? m[1] : uci.get('network', section_id, 'name');
return netDevs.filter(function(d) { return d.getName() == name })[0];
}
function getDevType(section_id) {
const dev = getDevice(section_id);
const cfg = uci.get('network', section_id);
const 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 'bonding':
return 'bonding';
case 'bridge':
return 'bridge';
case 'tunnel':
return 'tunnel';
case 'macvlan':
return 'macvlan';
case 'veth':
return 'veth';
case 'vrf':
return 'vrf';
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 'bonding':
return _('Aggregation device');
case 'bridge':
return _('Bridge device');
case 'tunnel':
return _('Tunnel device');
case 'macvlan':
return _('MAC VLAN');
case 'veth':
return _('Virtual Ethernet');
case 'vrf':
return _('Virtual Routing and Forwarding (VRF)');
default:
return _('Network device');
}
}
o = s.option(form.DummyValue, 'name', _('Device'));
o.modalonly = false;
o.textvalue = function(section_id) {
const dev = getDevice(section_id);
const ext = section_id.match(/^dev:/);
const 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) {
const dev = getDevice(section_id);
const val = uci.get('network', section_id, 'macaddr');
const 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) {
const dev = getDevice(section_id);
const val = uci.get('network', section_id, 'mtu');
const 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.Value, 'dhcp_default_duid', _('Default DUID'),
_('The default DUID for this device, used when acting as a DHCP server or client. The client identifier can be overridden on a per-interface basis.') + '
' +
_('This identifier is randomly generated the first time the device is booted.'));
o.datatype = 'and(rangelength(6,260),hexstring)';
o = s.option(form.Flag, 'multipath', _('Multi-Path TCP'), _('For packets originating from this device, e.g. VPN.'));
o.optional = true;
const l3mdevhelp1 = _('%s services running on this device in the default VRF context (ie., not bound to any VRF device) shall work across all VRF domains.');
const l3mdevhelp2 = _('Off means VRF traffic will be handled exclusively by sockets bound to VRFs.');
o = s.option(form.Flag, 'tcp_l3mdev', _('TCP Layer 3 Master Device (tcp_l3mdev) accept'),
l3mdevhelp1.format('TCP') + '
' +
l3mdevhelp2);
o = s.option(form.Flag, 'udp_l3mdev', _('UDP Layer 3 Master Device (udp_l3mdev) accept'),
l3mdevhelp1.format('UDP') + '
' +
l3mdevhelp2);
o = s.option(form.ListValue, 'packet_steering', _('Packet Steering'), _('Enable packet steering across CPUs. May help or hinder network speed.'));
o.value('0', _('Disabled'));
o.value('1',_('Enabled'));
o.value('2',_('Enabled (all CPUs)'));
o.default = '1';
o.optional = true;
const 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 (let 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) {
const sections = uci.sections('network', 'atm-bridge');
let max_unit = -1;
for (let s of sections) {
const unit = +s.unit;
if (!isNaN(unit) && unit > max_unit)
max_unit = unit;
}
return this.map.save(function() {
const 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() {
const section_ids = m.children[0].cfgsections();
const tasks = [];
for (let sid of section_ids) {
const row = nodes.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(sid));
const dsc = row.querySelector('[data-name="_ifacestat"] > div');
const btn1 = row.querySelector('.cbi-section-actions .reconnect');
const btn2 = row.querySelector('.cbi-section-actions .down');
if (dsc.getAttribute('reconnect') == '') {
dsc.setAttribute('reconnect', '1');
tasks.push(fs.exec('/sbin/ifup', [sid]).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', [sid]).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));
}
});