'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 && 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 <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') + ' &#160; '),
							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'),
								 _('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>&lt;numeric priority&gt; &lt;domain-name&gt; [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=&lt;seconds&gt;</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'), _('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.optional = true;
				}

				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;

		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';

		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));
	}
});
