'use strict';
'require fs';
'require ui';
'require dom';
'require uci';
'require rpc';
'require form';
'require network';
'require validation';
var generateKey = rpc.declare({
object: 'luci.wireguard',
method: 'generateKeyPair',
expect: { keys: {} }
});
var getPublicAndPrivateKeyFromPrivate = rpc.declare({
object: 'luci.wireguard',
method: 'getPublicAndPrivateKeyFromPrivate',
params: ['privkey'],
expect: { keys: {} }
});
var generatePsk = rpc.declare({
object: 'luci.wireguard',
method: 'generatePsk',
expect: { psk: '' }
});
var qrIcon = '';
function validateBase64(section_id, value) {
if (value.length == 0)
return true;
if (value.length != 44 || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/))
return _('Invalid Base64 key string');
if (value[43] != "=" )
return _('Invalid Base64 key string');
return true;
}
var stubValidator = {
factory: validation,
apply: function(type, value, args) {
if (value != null)
this.value = value;
return validation.types[type].apply(this, args);
},
assert: function(condition) {
return !!condition;
}
};
function generateDescription(name, texts) {
return E('li', { 'style': 'color: inherit;' }, [
E('span', name),
E('ul', texts.map(function (text) {
return E('li', { 'style': 'color: inherit;' }, text);
}))
]);
}
function invokeQREncode(data, code) {
return fs.exec_direct('/usr/bin/qrencode', [
'--inline', '--8bit', '--type=SVG',
'--output=-', '--', data
]).then(function(svg) {
code.style.opacity = '';
dom.content(code, Object.assign(E(svg), { style: 'width:100%;height:auto' }));
}).catch(function(error) {
code.style.opacity = '';
if (L.isObject(error) && error.name == 'NotFoundError') {
dom.content(code, [
Object.assign(E(qrIcon), { style: 'width:32px;height:32px;opacity:.2' }),
E('p', _('The qrencode package is required for generating an QR code image of the configuration.'))
]);
}
else {
dom.content(code, [
_('Unable to generate QR code: %s').format(L.isObject(error) ? error.message : error)
]);
}
});
}
var cbiKeyPairGenerate = form.DummyValue.extend({
cfgvalue: function(section_id, value) {
return E('button', {
'class': 'btn',
'click': ui.createHandlerFn(this, function(section_id, ev) {
var prv = this.section.getUIElement(section_id, 'private_key'),
pub = this.section.getUIElement(section_id, 'public_key'),
map = this.map;
if ((prv.getValue() || pub.getValue()) && !confirm(_('Do you want to replace the current keys?')))
return;
return generateKey().then(function(keypair) {
prv.setValue(keypair.priv);
pub.setValue(keypair.pub);
map.save(null, true);
});
}, section_id)
}, [ _('Generate new key pair') ]);
}
});
function handleWindowDragDropIgnore(ev) {
ev.preventDefault()
}
return network.registerProtocol('wireguard', {
getI18n: function() {
return _('WireGuard VPN');
},
getIfname: function() {
return this._ubus('l3_device') || this.sid;
},
getOpkgPackage: function() {
return 'wireguard-tools';
},
isFloating: function() {
return true;
},
isVirtual: function() {
return true;
},
getDevices: function() {
return null;
},
containsDevice: function(ifname) {
return (network.getIfnameOf(ifname) == this.getIfname());
},
renderFormOptions: function(s) {
var o, ss, ss2;
// -- general ---------------------------------------------------------------------
o = s.taboption('general', form.Value, 'private_key', _('Private Key'), _('Required. Base64-encoded private key for this interface.'));
o.password = true;
o.validate = validateBase64;
o.rmempty = false;
var serverName = this.getIfname();
o = s.taboption('general', form.Value, 'public_key', _('Public Key'), _('Base64-encoded public key of this interface for sharing.'));
o.rmempty = false;
o.write = function() {/* write nothing */};
o.load = function(section_id) {
var privKey = s.formvalue(section_id, 'private_key') || uci.get('network', section_id, 'private_key');
return getPublicAndPrivateKeyFromPrivate(privKey).then(
function(keypair) {
return keypair.pub || '';
},
function(error) {
return _('Error getting PublicKey');
}, this)
};
s.taboption('general', cbiKeyPairGenerate, '_gen_server_keypair', ' ');
o = s.taboption('general', form.Value, 'listen_port', _('Listen Port'), _('Optional. UDP port used for outgoing and incoming packets.'));
o.datatype = 'port';
o.placeholder = _('random');
o.optional = true;
o = s.taboption('general', form.DynamicList, 'addresses', _('IP Addresses'), _('Recommended. IP addresses of the WireGuard interface.'));
o.datatype = 'ipaddr';
o.optional = true;
o = s.taboption('general', form.Flag, 'nohostroute', _('No Host Routes'), _('Optional. Do not create host routes to peers.'));
o.optional = true;
o = s.taboption('general', form.Button, '_import', _('Import configuration'), _('Imports settings from an existing WireGuard configuration file'));
o.inputtitle = _('Load configuration…');
o.onclick = function() {
return ss.handleConfigImport('full');
};
// -- advanced --------------------------------------------------------------------
o = s.taboption('advanced', form.Value, 'mtu', _('MTU'), _('Optional. Maximum Transmission Unit of tunnel interface.'));
o.datatype = 'range(0,8940)';
o.placeholder = '1420';
o.optional = true;
o = s.taboption('advanced', form.Value, 'fwmark', _('Firewall Mark'), _('Optional. 32-bit mark for outgoing encrypted packets. Enter value in hex, starting with 0x
.'));
o.optional = true;
o.validate = function(section_id, value) {
if (value.length > 0 && !value.match(/^0x[a-fA-F0-9]{1,8}$/))
return _('Invalid hexadecimal value');
return true;
};
// -- peers -----------------------------------------------------------------------
try {
s.tab('peers', _('Peers'), _('Further information about WireGuard interfaces and peers at wireguard.com.'));
}
catch(e) {}
o = s.taboption('peers', form.SectionValue, '_peers', form.GridSection, 'wireguard_%s'.format(s.section));
o.depends('proto', 'wireguard');
ss = o.subsection;
ss.anonymous = true;
ss.addremove = true;
ss.addbtntitle = _('Add peer');
ss.nodescriptions = true;
ss.modaltitle = _('Edit peer');
ss.handleDragConfig = function(ev) {
ev.stopPropagation();
ev.preventDefault();
ev.dataTransfer.dropEffect = 'copy';
};
ss.handleDropConfig = function(mode, ev) {
var file = ev.dataTransfer.files[0],
nodes = ev.currentTarget,
input = nodes.querySelector('textarea'),
reader = new FileReader();
if (file) {
reader.onload = function(rev) {
input.value = rev.target.result.trim();
ss.handleApplyConfig(mode, nodes, file.name, ev);
};
reader.readAsText(file);
}
ev.stopPropagation();
ev.preventDefault();
};
ss.parseConfig = function(data) {
var lines = String(data).split(/(\r?\n)+/),
section = null,
config = { peers: [] },
s;
for (var i = 0; i < lines.length; i++) {
var line = lines[i].replace(/#.*$/, '').trim();
if (line.match(/^\[(\w+)\]$/)) {
section = RegExp.$1.toLowerCase();
if (section == 'peer')
config.peers.push(s = {});
else
s = config;
}
else if (section && line.match(/^(\w+)\s*=\s*(.+)$/)) {
var key = RegExp.$1,
val = RegExp.$2.trim();
if (val.length)
s[section + '_' + key.toLowerCase()] = val;
}
}
if (config.interface_address) {
config.interface_address = config.interface_address.split(/[, ]+/);
for (var i = 0; i < config.interface_address.length; i++)
if (!stubValidator.apply('ipaddr', config.interface_address[i]))
return _('Address setting is invalid');
}
if (config.interface_dns) {
config.interface_dns = config.interface_dns.split(/[, ]+/);
for (var i = 0; i < config.interface_dns.length; i++)
if (!stubValidator.apply('ipaddr', config.interface_dns[i], ['nomask']))
return _('DNS setting is invalid');
}
if (!config.interface_privatekey || validateBase64(null, config.interface_privatekey) !== true)
return _('PrivateKey setting is missing or invalid');
if (!stubValidator.apply('port', config.interface_listenport || '0'))
return _('ListenPort setting is invalid');
for (var i = 0; i < config.peers.length; i++) {
var pconf = config.peers[i];
if (pconf.peer_publickey != null && validateBase64(null, pconf.peer_publickey) !== true)
return _('PublicKey setting is invalid');
if (pconf.peer_presharedkey != null && validateBase64(null, pconf.peer_presharedkey) !== true)
return _('PresharedKey setting is invalid');
if (pconf.peer_allowedips) {
pconf.peer_allowedips = pconf.peer_allowedips.split(/[, ]+/);
for (var j = 0; j < pconf.peer_allowedips.length; j++)
if (!stubValidator.apply('ipaddr', pconf.peer_allowedips[j]))
return _('AllowedIPs setting is invalid');
}
else {
pconf.peer_allowedips = [ '0.0.0.0/0', '::/0' ];
}
if (pconf.peer_endpoint) {
var host_port = pconf.peer_endpoint.match(/^\[([a-fA-F0-9:]+)\]:(\d+)$/) || pconf.peer_endpoint.match(/^(.+):(\d+)$/);
if (!host_port || !stubValidator.apply('host', host_port[1]) || !stubValidator.apply('port', host_port[2]))
return _('Endpoint setting is invalid');
pconf.peer_endpoint = [ host_port[1], host_port[2] ];
}
if (pconf.peer_persistentkeepalive == 'off' || pconf.peer_persistentkeepalive == '0')
delete pconf.peer_persistentkeepalive;
if (!stubValidator.apply('port', pconf.peer_persistentkeepalive || '0'))
return _('PersistentKeepAlive setting is invalid');
}
return config;
};
ss.handleApplyConfig = function(mode, nodes, comment, ev) {
var input = nodes.querySelector('textarea').value,
error = nodes.querySelector('.alert-message'),
cancel = nodes.nextElementSibling.querySelector('.btn'),
config = this.parseConfig(input);
if (typeof(config) == 'string') {
error.firstChild.data = _('Cannot parse configuration: %s').format(config);
error.style.display = 'block';
return;
}
if (mode == 'full') {
var prv = s.formvalue(s.section, 'private_key');
if (prv && prv != config.interface_privatekey && !confirm(_('Overwrite the current settings with the imported configuration?')))
return;
return getPublicAndPrivateKeyFromPrivate(config.interface_privatekey).then(function(keypair) {
s.getOption('private_key').getUIElement(s.section).setValue(keypair.priv);
s.getOption('public_key').getUIElement(s.section).setValue(keypair.pub);
s.getOption('listen_port').getUIElement(s.section).setValue(config.interface_listenport || '');
s.getOption('addresses').getUIElement(s.section).setValue(config.interface_address);
if (config.interface_dns)
s.getOption('dns').getUIElement(s.section).setValue(config.interface_dns);
for (var i = 0; i < config.peers.length; i++) {
var pconf = config.peers[i];
var sid = uci.add('network', 'wireguard_' + s.section);
uci.sections('network', 'wireguard_' + s.section, function(peer) {
if (peer.public_key == pconf.peer_publickey)
uci.remove('network', peer['.name']);
});
uci.set('network', sid, 'description', comment || _('Imported peer configuration'));
uci.set('network', sid, 'public_key', pconf.peer_publickey);
uci.set('network', sid, 'preshared_key', pconf.peer_presharedkey);
uci.set('network', sid, 'allowed_ips', pconf.peer_allowedips);
uci.set('network', sid, 'persistent_keepalive', pconf.peer_persistentkeepalive);
if (pconf.peer_endpoint) {
uci.set('network', sid, 'endpoint_host', pconf.peer_endpoint[0]);
uci.set('network', sid, 'endpoint_port', pconf.peer_endpoint[1]);
}
}
return s.map.save(null, true);
}).then(function() {
cancel.click();
});
}
else {
return getPublicAndPrivateKeyFromPrivate(config.interface_privatekey).then(function(keypair) {
var sid = uci.add('network', 'wireguard_' + s.section);
var pub = s.formvalue(s.section, 'public_key');
uci.sections('network', 'wireguard_' + s.section, function(peer) {
if (peer.public_key == keypair.pub)
uci.remove('network', peer['.name']);
});
uci.set('network', sid, 'description', comment || _('Imported peer configuration'));
uci.set('network', sid, 'public_key', keypair.pub);
uci.set('network', sid, 'private_key', keypair.priv);
for (var i = 0; i < config.peers.length; i++) {
var pconf = config.peers[i];
if (pconf.peer_publickey == pub) {
uci.set('network', sid, 'preshared_key', pconf.peer_presharedkey);
uci.set('network', sid, 'allowed_ips', pconf.peer_allowedips);
uci.set('network', sid, 'persistent_keepalive', pconf.peer_persistentkeepalive);
break;
}
}
return s.map.save(null, true);
}).then(function() {
cancel.click();
});
}
};
ss.handleConfigImport = function(mode) {
var mapNode = ss.getActiveModalMap(),
headNode = mapNode.parentNode.querySelector('h4'),
parent = this.map;
var nodes = E('div', {
'dragover': this.handleDragConfig,
'drop': this.handleDropConfig.bind(this, mode)
}, [
E([], (mode == 'full') ? [
E('p', _('Drag or paste a valid *.conf file below to configure the local WireGuard interface.'))
] : [
E('p', _('Paste or drag a WireGuard configuration (commonly wg0.conf) from another system below to create a matching peer entry allowing that system to connect to the local WireGuard interface.')),
E('p', _('To fully configure the local WireGuard interface from an existing (e.g. provider supplied) configuration file, use the configuration import instead.'))
]),
E('p', [
E('textarea', {
'placeholder': (mode == 'full')
? _('Paste or drag supplied WireGuard configuration file…')
: _('Paste or drag WireGuard peer configuration (wg0.conf) file…'),
'style': 'height:5em;width:100%; white-space:pre'
})
]),
E('div', {
'class': 'alert-message',
'style': 'display:none'
}, [''])
]);
var cancelFn = function() {
nodes.parentNode.removeChild(nodes.nextSibling);
nodes.parentNode.removeChild(nodes);
mapNode.classList.remove('hidden');
mapNode.nextSibling.classList.remove('hidden');
headNode.removeChild(headNode.lastChild);
window.removeEventListener('dragover', handleWindowDragDropIgnore);
window.removeEventListener('drop', handleWindowDragDropIgnore);
};
var a = nodes.querySelector('a.full-import');
if (a) {
a.addEventListener('click', ui.createHandlerFn(this, function(mode) {
cancelFn();
this.handleConfigImport('full');
}));
}
mapNode.classList.add('hidden');
mapNode.nextElementSibling.classList.add('hidden');
headNode.appendChild(E('span', [ ' » ', (mode == 'full') ? _('Import configuration') : _('Import as peer') ]));
mapNode.parentNode.appendChild(E([], [
nodes,
E('div', {
'class': 'right'
}, [
E('button', {
'class': 'btn',
'click': cancelFn
}, [ _('Cancel') ]),
' ',
E('button', {
'class': 'btn primary',
'click': ui.createHandlerFn(this, 'handleApplyConfig', mode, nodes, null)
}, [ _('Import settings') ])
])
]));
window.addEventListener('dragover', handleWindowDragDropIgnore);
window.addEventListener('drop', handleWindowDragDropIgnore);
};
ss.renderSectionAdd = function(/* ... */) {
var nodes = this.super('renderSectionAdd', arguments);
nodes.appendChild(E('button', {
'class': 'btn',
'click': ui.createHandlerFn(this, 'handleConfigImport', 'peer')
}, [ _('Import configuration as peer…') ]));
return nodes;
};
ss.renderSectionPlaceholder = function() {
return E('em', _('No peers defined yet.'));
};
o = ss.option(form.Flag, 'disabled', _('Peer disabled'), _('Enable / Disable peer. Restart wireguard interface to apply changes.'));
o.modalonly = true;
o.optional = true;
o = ss.option(form.Value, 'description', _('Description'), _('Optional. Description of peer.'));
o.placeholder = 'My Peer';
o.datatype = 'string';
o.optional = true;
o.width = '30%';
o.textvalue = function(section_id) {
var dis = ss.getOption('disabled'),
pub = ss.getOption('public_key'),
prv = ss.getOption('private_key'),
psk = ss.getOption('preshared_key'),
name = this.cfgvalue(section_id),
key = pub.cfgvalue(section_id);
var desc = [
E('p', [
name ? E('span', [ name ]) : E('em', [ _('Untitled peer') ])
])
];
if (dis.cfgvalue(section_id) == '1')
desc.push(E('span', {
'class': 'ifacebadge',
'data-tooltip': _('WireGuard peer is disabled')
}, [
E('em', [ _('Disabled', 'Label indicating that WireGuard peer is disabled') ])
]), ' ');
if (!key || !pub.isValid(section_id)) {
desc.push(E('span', {
'class': 'ifacebadge',
'data-tooltip': _('Public key is missing')
}, [
E('em', [ _('Key missing', 'Label indicating that WireGuard peer lacks public key') ])
]));
}
else {
desc.push(
E('span', {
'class': 'ifacebadge',
'data-tooltip': _('Public key: %h', 'Tooltip displaying full WireGuard peer public key').format(key)
}, [
E('code', [ key.replace(/^(.{5}).+(.{6})$/, '$1…$2') ])
]),
' ',
(prv.cfgvalue(section_id) && prv.isValid(section_id))
? E('span', {
'class': 'ifacebadge',
'data-tooltip': _('Private key present')
}, [ _('Private', 'Label indicating that WireGuard peer private key is stored') ]) : '',
' ',
(psk.cfgvalue(section_id) && psk.isValid(section_id))
? E('span', {
'class': 'ifacebadge',
'data-tooltip': _('Preshared key in use')
}, [ _('PSK', 'Label indicating that WireGuard peer uses a PSK') ]) : ''
);
}
return E([], desc);
};
function handleKeyChange(ev, section_id, value) {
var prv = this.section.getUIElement(section_id, 'private_key'),
btn = this.map.findElement('.btn.qr-code');
btn.disabled = (!prv.isValid() || !prv.getValue());
}
o = ss.option(form.Value, 'public_key', _('Public Key'), _('Required. Public key of the WireGuard peer.'));
o.modalonly = true;
o.validate = validateBase64;
o.onchange = handleKeyChange;
o = ss.option(form.Value, 'private_key', _('Private Key'), _('Optional. Private key of the WireGuard peer. The key is not required for establishing a connection but allows generating a peer configuration or QR code if available. It can be removed after the configuration has been exported.'));
o.modalonly = true;
o.validate = validateBase64;
o.onchange = handleKeyChange;
o.password = true;
o = ss.option(cbiKeyPairGenerate, '_gen_peer_keypair', ' ');
o.modalonly = true;
o = ss.option(form.Value, 'preshared_key', _('Preshared Key'), _('Optional. Base64-encoded preshared key. Adds in an additional layer of symmetric-key cryptography for post-quantum resistance.'));
o.modalonly = true;
o.validate = validateBase64;
o.password = true;
o = ss.option(form.DummyValue, '_gen_psk', ' ');
o.modalonly = true;
o.cfgvalue = function(section_id, value) {
return E('button', {
'class': 'btn',
'click': ui.createHandlerFn(this, function(section_id, ev) {
var psk = this.section.getUIElement(section_id, 'preshared_key'),
map = this.map;
if (psk.getValue() && !confirm(_('Do you want to replace the current PSK?')))
return;
return generatePsk().then(function(key) {
psk.setValue(key);
map.save(null, true);
});
}, section_id)
}, [ _('Generate preshared key') ]);
};
o = ss.option(form.DynamicList, 'allowed_ips', _('Allowed IPs'), _("Optional. IP addresses and prefixes that this peer is allowed to use inside the tunnel. Usually the peer's tunnel IP addresses and the networks the peer routes through the tunnel."));
o.datatype = 'ipaddr';
o.textvalue = function(section_id) {
var ips = L.toArray(this.cfgvalue(section_id)),
list = [];
for (var i = 0; i < ips.length; i++) {
if (i > 7) {
list.push(E('em', {
'class': 'ifacebadge cbi-tooltip-container'
}, [
_('+ %d more', 'Label indicating further amount of allowed ips').format(ips.length - i),
E('span', {
'class': 'cbi-tooltip'
}, [
E('ul', ips.map(function(ip) {
return E('li', [
E('span', { 'class': 'ifacebadge' }, [ ip ])
]);
}))
])
]));
break;
}
list.push(E('span', { 'class': 'ifacebadge' }, [ ips[i] ]));
}
if (!list.length)
list.push('*');
return E('span', { 'style': 'display:inline-flex;flex-wrap:wrap;gap:.125em' }, list);
};
o = ss.option(form.Flag, 'route_allowed_ips', _('Route Allowed IPs'), _('Optional. Create routes for Allowed IPs for this peer.'));
o.modalonly = true;
o = ss.option(form.Value, 'endpoint_host', _('Endpoint Host'), _('Optional. Host of peer. Names are resolved prior to bringing up the interface.'));
o.placeholder = 'vpn.example.com';
o.datatype = 'host';
o.textvalue = function(section_id) {
var host = this.cfgvalue(section_id),
port = this.section.cfgvalue(section_id, 'endpoint_port');
return (host && port)
? '%h:%d'.format(host, port)
: (host
? '%h:*'.format(host)
: (port
? '*:%d'.format(port)
: '*'));
};
o = ss.option(form.Value, 'endpoint_port', _('Endpoint Port'), _('Optional. Port of peer.'));
o.modalonly = true;
o.placeholder = '51820';
o.datatype = 'port';
o = ss.option(form.Value, 'persistent_keepalive', _('Persistent Keep Alive'), _('Optional. Seconds between keep alive messages. Default is 0 (disabled). Recommended value if this device is behind a NAT is 25.'));
o.modalonly = true;
o.datatype = 'range(0,65535)';
o.placeholder = '0';
o = ss.option(form.DummyValue, '_keyops', _('Configuration Export'),
_('Generates a configuration suitable for import on a WireGuard peer'));
o.modalonly = true;
o.createPeerConfig = function(section_id, endpoint, ips) {
var pub = s.formvalue(s.section, 'public_key'),
port = s.formvalue(s.section, 'listen_port') || '51820',
prv = this.section.formvalue(section_id, 'private_key'),
psk = this.section.formvalue(section_id, 'preshared_key'),
eport = this.section.formvalue(section_id, 'endpoint_port'),
keep = this.section.formvalue(section_id, 'persistent_keepalive');
// If endpoint is IPv6 we must escape it with []
if (endpoint.indexOf(':') > 0) {
endpoint = '['+endpoint+']';
}
return [
'[Interface]',
'PrivateKey = ' + prv,
eport ? 'ListenPort = ' + eport : '# ListenPort not defined',
'',
'[Peer]',
'PublicKey = ' + pub,
psk ? 'PresharedKey = ' + psk : '# PresharedKey not used',
ips && ips.length ? 'AllowedIPs = ' + ips.join(', ') : '# AllowedIPs not defined',
endpoint ? 'Endpoint = ' + endpoint + ':' + port : '# Endpoint not defined',
keep ? 'PersistentKeepAlive = ' + keep : '# PersistentKeepAlive not defined'
].join('\n');
};
o.handleGenerateQR = function(section_id, ev) {
var mapNode = ss.getActiveModalMap(),
headNode = mapNode.parentNode.querySelector('h4'),
configGenerator = this.createPeerConfig.bind(this, section_id),
parent = this.map;
return Promise.all([
network.getWANNetworks(),
network.getWAN6Networks(),
L.resolveDefault(uci.load('ddns')),
L.resolveDefault(uci.load('system')),
parent.save(null, true)
]).then(function(data) {
var hostnames = [];
uci.sections('ddns', 'service', function(s) {
if (typeof(s.lookup_host) == 'string' && s.enabled == '1')
hostnames.push(s.lookup_host);
});
uci.sections('system', 'system', function(s) {
if (typeof(s.hostname) == 'string' && s.hostname.indexOf('.') > 0)
hostnames.push(s.hostname);
});
for (var i = 0; i < data[0].length; i++)
hostnames.push.apply(hostnames, data[0][i].getIPAddrs().map(function(ip) { return ip.split('/')[0] }));
for (var i = 0; i < data[1].length; i++)
hostnames.push.apply(hostnames, data[1][i].getIP6Addrs().map(function(ip) { return ip.split('/')[0] }));
var ips = [ '0.0.0.0/0', '::/0' ];
var qrm, qrs, qro;
qrm = new form.JSONMap({ config: { endpoint: hostnames[0], allowed_ips: ips } }, null, _('The generated configuration can be imported into a WireGuard client application to set up a connection towards this device.'));
qrm.parent = parent;
qrs = qrm.section(form.NamedSection, 'config');
function handleConfigChange(ev, section_id, value) {
var code = this.map.findElement('.qr-code'),
conf = this.map.findElement('.client-config'),
endpoint = this.section.getUIElement(section_id, 'endpoint'),
ips = this.section.getUIElement(section_id, 'allowed_ips');
if (this.isValid(section_id)) {
conf.firstChild.data = configGenerator(endpoint.getValue(), ips.getValue());
code.style.opacity = '.5';
invokeQREncode(conf.firstChild.data, code);
}
};
qro = qrs.option(form.Value, 'endpoint', _('Connection endpoint'), _('The public hostname or IP address of this system the peer should connect to. This usually is a static public IP address, a static hostname or a DDNS domain.'));
qro.datatype = 'or(ipaddr,hostname)';
hostnames.forEach(function(hostname) { qro.value(hostname) });
qro.onchange = handleConfigChange;
qro = qrs.option(form.DynamicList, 'allowed_ips', _('Allowed IPs'), _('IP addresses that are allowed inside the tunnel. The peer will accept tunnelled packets with source IP addresses matching this list and route back packets with matching destination IP.'));
qro.datatype = 'ipaddr';
qro.default = ips;
ips.forEach(function(ip) { qro.value(ip) });
qro.onchange = handleConfigChange;
qro = qrs.option(form.DummyValue, 'output');
qro.renderWidget = function() {
var peer_config = configGenerator(hostnames[0], ips);
var node = E('div', {
'style': 'display:flex;flex-wrap:wrap;align-items:center;gap:.5em;width:100%'
}, [
E('div', {
'class': 'qr-code',
'style': 'width:320px;flex:0 1 320px;text-align:center'
}, [
E('em', { 'class': 'spinning' }, [ _('Generating QR code…') ])
]),
E('pre', {
'class': 'client-config',
'style': 'flex:1;white-space:pre;overflow:auto',
'click': function(ev) {
var sel = window.getSelection(),
range = document.createRange();
range.selectNodeContents(ev.currentTarget);
sel.removeAllRanges();
sel.addRange(range);
}
}, [ peer_config ])
]);
invokeQREncode(peer_config, node.firstChild);
return node;
};
return qrm.render().then(function(nodes) {
mapNode.classList.add('hidden');
mapNode.nextElementSibling.classList.add('hidden');
headNode.appendChild(E('span', [ ' » ', _('Generate configuration') ]));
mapNode.parentNode.appendChild(E([], [
nodes,
E('div', {
'class': 'right'
}, [
E('button', {
'class': 'btn',
'click': function() {
nodes.parentNode.removeChild(nodes.nextSibling);
nodes.parentNode.removeChild(nodes);
mapNode.classList.remove('hidden');
mapNode.nextSibling.classList.remove('hidden');
headNode.removeChild(headNode.lastChild);
}
}, [ _('Back to peer configuration') ])
])
]));
if (!s.formvalue(s.section, 'listen_port')) {
nodes.appendChild(E('div', { 'class': 'alert-message' }, [
E('p', [
_('No fixed interface listening port defined, peers might not be able to initiate connections to this WireGuard instance!')
])
]));
}
});
});
};
o.cfgvalue = function(section_id, value) {
var privkey = this.section.cfgvalue(section_id, 'private_key');
return E('button', {
'class': 'btn qr-code',
'style': 'display:inline-flex;align-items:center;gap:.5em',
'click': ui.createHandlerFn(this, 'handleGenerateQR', section_id),
'disabled': privkey ? null : ''
}, [
Object.assign(E(qrIcon), { style: 'width:22px;height:22px' }),
_('Generate configuration…')
]);
};
},
deleteConfiguration: function() {
uci.sections('network', 'wireguard_%s'.format(this.sid), function(s) {
uci.remove('network', s['.name']);
});
}
});