mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-28 12:02:00 +08:00
1966 lines
73 KiB
JavaScript
1966 lines
73 KiB
JavaScript
'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':
|
|
case 'quectel':
|
|
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 <em>unspecified</em> to remove the interface from the associated zone or fill out the <em>custom</em> field to define a new zone and attach the interface to it.'));
|
|
o.network = ifc.getName();
|
|
o.optional = true;
|
|
|
|
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 <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> for this interface (dnsmasq only).'));
|
|
}
|
|
|
|
if (protoval == 'static') {
|
|
so = ss.taboption('general', form.Flag, 'dynamicdhcp', _('Dynamic <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>'), _('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 (<code>2m</code>).'));
|
|
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 "<code>6,192.168.2.1,192.168.2.2</code>" 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('<a href="%s" target="_blank">RFC8925</a>').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', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-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 <em>relay mode</em> if a designated master interface is configured and active, otherwise fall back to <em>server mode</em>.');
|
|
const ndp_downstream_desc = _('Operate in <em>relay mode</em> if a designated master interface is configured and active, otherwise disable <abbr title="Neighbour Discovery Protocol">NDP</abbr> proxying.');
|
|
const hybrid_master_desc = _('Operate in <em>relay mode</em> 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', _('<abbr title="Router Advertisement">RA</abbr>-Service'),
|
|
_('Configures the operation mode of the <abbr title="Router Advertisement">RA</abbr> service on this interface.'));
|
|
so.optional = true;
|
|
so.value('', _('disabled'),
|
|
_('Do not send any <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages on this interface.'));
|
|
so.value('server', _('server mode'),
|
|
_('Send <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages advertising this device as IPv6 router.'));
|
|
so.value('relay', _('relay mode'),
|
|
_('Forward <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> 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 <abbr title="Router Advertisement">RA</abbr> 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 <abbr title="Stateless Address Auto Config">SLAAC</abbr>'),
|
|
_('Set the autonomous address-configuration flag in the prefix information options of sent <abbr title="Router Advertisement">RA</abbr> 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', _('<abbr title="Router Advertisement">RA</abbr> Flags'),
|
|
_('Specifies the flags sent in <abbr title="Router Advertisement">RA</abbr> messages, for example to instruct clients to request further information via stateful DHCPv6.'));
|
|
so.optional = true;
|
|
so.value('managed-config', _('managed config (M)'),
|
|
_('The <em>Managed address configuration</em> (M) flag indicates that IPv6 addresses are available via DHCPv6.'));
|
|
so.value('other-config', _('other config (O)'),
|
|
_('The <em>Other configuration</em> (O) flag indicates that other information, such as DNS servers, is available via DHCPv6.'));
|
|
so.value('home-agent', _('mobile home agent (H)'),
|
|
_('The <em>Mobile IPv6 Home Agent</em> (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 <abbr title="Prefix Information Option">PIO</abbr> Flags'),
|
|
_('Allow <abbr title="Prefix Information Option">PIO</abbr> flags sent in RA messages, globally for all prefixes.'));
|
|
so.optional = true;
|
|
so.value('pd', _('prefix delegation (PD) preferred (P)'),
|
|
_('The PD <em>preferred</em> (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 <abbr title="Router Advertisement">RA</abbr> messages.') + ' ' +
|
|
_('See %s and %s.').format('<a href="%s" target="_blank">RFC6146</a>', '<a href="%s" target="_blank">RFC8781</a>').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 <abbr title="Router Advertisement">RA</abbr> interval'), _('Maximum time allowed between sending unsolicited <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr>. Default is 600 seconds.'));
|
|
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 <abbr title="Router Advertisement">RA</abbr> interval'), _('Minimum time allowed between sending unsolicited <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr>. Default is 200 seconds.'));
|
|
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', _('<abbr title="Router Advertisement">RA</abbr> Reachability Timer'),
|
|
_('Units: milliseconds. 0 means unspecified.') + ' ' +
|
|
_('Dictates how long a node assumes a neighbor is reachable after a reachability confirmation; published in <abbr title="Router Advertisement">RA</abbr> 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', _('<abbr title="Router Advertisement">RA</abbr> Retransmission Timer'),
|
|
_('Units: milliseconds. 0 means unspecified.') + ' ' +
|
|
_('Controls retransmitted Neighbor Solicitation messages; published in <abbr title="Router Advertisement">RA</abbr> 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', _('<abbr title="Router Advertisement">RA</abbr> Lifetime'), _('Router Lifetime published in <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> 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', _('<abbr title="Router Advertisement">RA</abbr> MTU'), _('The <abbr title="Maximum Transmission Unit">MTU</abbr> to be published in <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages. 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', _('<abbr title="Router Advertisement">RA</abbr> Hop Limit'), _('The maximum hops to be published in <abbr title="Router Advertisement">RA</abbr> 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-<abbr title="Prefix Delegation">PD</abbr>'),
|
|
_('Toggle IPv6 PD via DHCPv6.'));
|
|
so.depends({ dhcpv6: 'server' });
|
|
|
|
so = ss.taboption('dhcpv6', form.Value, 'dhcpv6_pd_min_len', _('<abbr title="Prefix Delegation">PD</abbr> 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.') + '<br/>' +
|
|
_('Specifies a fixed list of DNS server addresses to announce via RA.') + '<br/>' +
|
|
_('If left unspecified, the device will announce itself as DNS server unless the <em>Local IPv6 DNS server</em> 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/<abbr title="Router Advertisement">RA</abbr> (see %s).')
|
|
.format('<a href="%s" target="_blank">RFC9463</a>').format('https://www.rfc-editor.org/rfc/rfc9463') + '<br/>' +
|
|
_('IPv4 addresses are only supported if <code>odhcpd</code> also handles DHCPv4.') + '<br/>' +
|
|
_('Syntax: <code><numeric priority> <domain-name> [IP,...] [SVC parameter ...]</code>') + '<br/>' +
|
|
_('Example: <code>100 dns.example.com 2001:db8::53,192.168.1.53 alpn=doq port=853</code>') + '<br/>' +
|
|
_('Note: the <code>_lifetime=<seconds></code> SVC parameter sets the lifetime of the announced server (use <code>0</code> 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('<a href="%s" target="_blank">RFC5908</a>').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', _('<abbr title="Neighbour Discovery Protocol">NDP</abbr>-Proxy'),
|
|
_('Configures the operation mode of the NDP proxy service on this interface.'));
|
|
so.optional = true;
|
|
so.value('', _('disabled'),
|
|
_('Do not proxy any <abbr title="Neighbour Discovery Protocol">NDP</abbr> packets.'));
|
|
so.value('relay', _('relay mode'),
|
|
_('Forward <abbr title="Neighbour Discovery Protocol">NDP</abbr> <abbr title="Neighbour Solicitation, Type 135">NS</abbr> and <abbr title="Neighbour Advertisement, Type 136">NA</abbr> 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';
|
|
|
|
if (L.hasSystemFeature('mptcp')) {
|
|
o = nettools.replaceOption(s, 'advanced', form.RichListValue, 'multipath', _('Multi-Path TCP'),
|
|
_('Multi-Path TCP') + ' %s'.format('<a href="%s" target="_blank">RFC8684</a>').format('https://www.rfc-editor.org/rfc/rfc8684.html') + '<br/>' +
|
|
_('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');
|
|
|
|
/* // The kernel takes max 15 character names. We can skip this.
|
|
const pr = network.getProtocol(proto.formvalue(section_id), value);
|
|
const 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) {
|
|
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 <code>fd00::/8</code> (the L bit is always 1).').format('<a href="%s" target="_blank">RFC4193</a>').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 <abbr title="DHCP Unique Identifier">DUID</abbr> for this device, used when acting as a DHCP server or client. The client identifier can be overridden on a per-interface basis.') + '<br />' +
|
|
_('This identifier is randomly generated the first time the device is booted.'));
|
|
o.datatype = 'and(rangelength(6,260),hexstring)';
|
|
|
|
if (L.hasSystemFeature('mptcp')) {
|
|
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') + '<br/>' +
|
|
l3mdevhelp2);
|
|
|
|
o = s.option(form.Flag, 'udp_l3mdev', _('UDP Layer 3 Master Device (udp_l3mdev) accept'),
|
|
l3mdevhelp1.format('UDP') + '<br/>' +
|
|
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 (<abbr title="Receive Packet Steering">RPS</abbr>)'),
|
|
_('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));
|
|
}
|
|
});
|