11 var generateKey
= rpc
.declare({
12 object
: 'luci.wireguard',
13 method
: 'generateKeyPair',
17 var getPublicAndPrivateKeyFromPrivate
= rpc
.declare({
18 object
: 'luci.wireguard',
19 method
: 'getPublicAndPrivateKeyFromPrivate',
24 var generatePsk
= rpc
.declare({
25 object
: 'luci.wireguard',
26 method
: 'generatePsk',
30 var qrIcon
= '<svg viewBox="0 0 29 29" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M0 0h29v29H0z"/><path d="M4 4h1v1H4zM5 4h1v1H5zM6 4h1v1H6zM7 4h1v1H7zM8 4h1v1H8zM9 4h1v1H9zM10 4h1v1h-1zM12 4h1v1h-1zM13 4h1v1h-1zM14 4h1v1h-1zM15 4h1v1h-1zM16 4h1v1h-1zM18 4h1v1h-1zM19 4h1v1h-1zM20 4h1v1h-1zM21 4h1v1h-1zM22 4h1v1h-1zM23 4h1v1h-1zM24 4h1v1h-1zM4 5h1v1H4zM10 5h1v1h-1zM12 5h1v1h-1zM14 5h1v1h-1zM16 5h1v1h-1zM18 5h1v1h-1zM24 5h1v1h-1zM4 6h1v1H4zM6 6h1v1H6zM7 6h1v1H7zM8 6h1v1H8zM10 6h1v1h-1zM12 6h1v1h-1zM18 6h1v1h-1zM20 6h1v1h-1zM21 6h1v1h-1zM22 6h1v1h-1zM24 6h1v1h-1zM4 7h1v1H4zM6 7h1v1H6zM7 7h1v1H7zM8 7h1v1H8zM10 7h1v1h-1zM12 7h1v1h-1zM13 7h1v1h-1zM14 7h1v1h-1zM15 7h1v1h-1zM18 7h1v1h-1zM20 7h1v1h-1zM21 7h1v1h-1zM22 7h1v1h-1zM24 7h1v1h-1zM4 8h1v1H4zM6 8h1v1H6zM7 8h1v1H7zM8 8h1v1H8zM10 8h1v1h-1zM16 8h1v1h-1zM18 8h1v1h-1zM20 8h1v1h-1zM21 8h1v1h-1zM22 8h1v1h-1zM24 8h1v1h-1zM4 9h1v1H4zM10 9h1v1h-1zM12 9h1v1h-1zM13 9h1v1h-1zM15 9h1v1h-1zM18 9h1v1h-1zM24 9h1v1h-1zM4 10h1v1H4zM5 10h1v1H5zM6 10h1v1H6zM7 10h1v1H7zM8 10h1v1H8zM9 10h1v1H9zM10 10h1v1h-1zM12 10h1v1h-1zM14 10h1v1h-1zM16 10h1v1h-1zM18 10h1v1h-1zM19 10h1v1h-1zM20 10h1v1h-1zM21 10h1v1h-1zM22 10h1v1h-1zM23 10h1v1h-1zM24 10h1v1h-1zM13 11h1v1h-1zM14 11h1v1h-1zM15 11h1v1h-1zM16 11h1v1h-1zM4 12h1v1H4zM5 12h1v1H5zM8 12h1v1H8zM9 12h1v1H9zM10 12h1v1h-1zM13 12h1v1h-1zM15 12h1v1h-1zM19 12h1v1h-1zM21 12h1v1h-1zM22 12h1v1h-1zM23 12h1v1h-1zM24 12h1v1h-1zM5 13h1v1H5zM6 13h1v1H6zM8 13h1v1H8zM11 13h1v1h-1zM13 13h1v1h-1zM14 13h1v1h-1zM15 13h1v1h-1zM16 13h1v1h-1zM19 13h1v1h-1zM22 13h1v1h-1zM4 14h1v1H4zM5 14h1v1H5zM9 14h1v1H9zM10 14h1v1h-1zM11 14h1v1h-1zM15 14h1v1h-1zM18 14h1v1h-1zM19 14h1v1h-1zM20 14h1v1h-1zM21 14h1v1h-1zM22 14h1v1h-1zM23 14h1v1h-1zM7 15h1v1H7zM8 15h1v1H8zM9 15h1v1H9zM11 15h1v1h-1zM12 15h1v1h-1zM13 15h1v1h-1zM17 15h1v1h-1zM18 15h1v1h-1zM20 15h1v1h-1zM21 15h1v1h-1zM23 15h1v1h-1zM4 16h1v1H4zM6 16h1v1H6zM10 16h1v1h-1zM11 16h1v1h-1zM13 16h1v1h-1zM14 16h1v1h-1zM16 16h1v1h-1zM17 16h1v1h-1zM18 16h1v1h-1zM22 16h1v1h-1zM23 16h1v1h-1zM24 16h1v1h-1zM12 17h1v1h-1zM16 17h1v1h-1zM17 17h1v1h-1zM18 17h1v1h-1zM4 18h1v1H4zM5 18h1v1H5zM6 18h1v1H6zM7 18h1v1H7zM8 18h1v1H8zM9 18h1v1H9zM10 18h1v1h-1zM14 18h1v1h-1zM16 18h1v1h-1zM17 18h1v1h-1zM21 18h1v1h-1zM22 18h1v1h-1zM23 18h1v1h-1zM4 19h1v1H4zM10 19h1v1h-1zM12 19h1v1h-1zM13 19h1v1h-1zM15 19h1v1h-1zM16 19h1v1h-1zM19 19h1v1h-1zM21 19h1v1h-1zM23 19h1v1h-1zM24 19h1v1h-1zM4 20h1v1H4zM6 20h1v1H6zM7 20h1v1H7zM8 20h1v1H8zM10 20h1v1h-1zM12 20h1v1h-1zM13 20h1v1h-1zM15 20h1v1h-1zM18 20h1v1h-1zM19 20h1v1h-1zM20 20h1v1h-1zM22 20h1v1h-1zM23 20h1v1h-1zM24 20h1v1h-1zM4 21h1v1H4zM6 21h1v1H6zM7 21h1v1H7zM8 21h1v1H8zM10 21h1v1h-1zM13 21h1v1h-1zM15 21h1v1h-1zM16 21h1v1h-1zM19 21h1v1h-1zM21 21h1v1h-1zM23 21h1v1h-1zM24 21h1v1h-1zM4 22h1v1H4zM6 22h1v1H6zM7 22h1v1H7zM8 22h1v1H8zM10 22h1v1h-1zM13 22h1v1h-1zM15 22h1v1h-1zM18 22h1v1h-1zM19 22h1v1h-1zM20 22h1v1h-1zM21 22h1v1h-1zM22 22h1v1h-1zM4 23h1v1H4zM10 23h1v1h-1zM12 23h1v1h-1zM13 23h1v1h-1zM14 23h1v1h-1zM17 23h1v1h-1zM18 23h1v1h-1zM20 23h1v1h-1zM22 23h1v1h-1zM4 24h1v1H4zM5 24h1v1H5zM6 24h1v1H6zM7 24h1v1H7zM8 24h1v1H8zM9 24h1v1H9zM10 24h1v1h-1zM12 24h1v1h-1zM13 24h1v1h-1zM14 24h1v1h-1zM16 24h1v1h-1zM17 24h1v1h-1zM18 24h1v1h-1zM22 24h1v1h-1zM24 24h1v1h-1z"/></svg>';
32 function validateBase64(section_id
, value
) {
33 if (value
.length
== 0)
36 if (value
.length
!= 44 || !value
.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/))
37 return _('Invalid Base64 key string');
39 if (value
[43] != "=" )
40 return _('Invalid Base64 key string');
47 apply: function(type
, value
, args
) {
51 return validation
.types
[type
].apply(this, args
);
53 assert: function(condition
) {
58 function generateDescription(name
, texts
) {
59 return E('li', { 'style': 'color: inherit;' }, [
61 E('ul', texts
.map(function (text
) {
62 return E('li', { 'style': 'color: inherit;' }, text
);
67 function invokeQREncode(data
, code
) {
68 return fs
.exec_direct('/usr/bin/qrencode', [
69 '--inline', '--8bit', '--type=SVG',
70 '--output=-', '--', data
71 ]).then(function(svg
) {
72 code
.style
.opacity
= '';
73 dom
.content(code
, Object
.assign(E(svg
), { style
: 'width:100%;height:auto' }));
74 }).catch(function(error
) {
75 code
.style
.opacity
= '';
77 if (L
.isObject(error
) && error
.name
== 'NotFoundError') {
79 Object
.assign(E(qrIcon
), { style
: 'width:32px;height:32px;opacity:.2' }),
80 E('p', _('The <em>qrencode</em> package is required for generating an QR code image of the configuration.'))
85 _('Unable to generate QR code: %s').format(L
.isObject(error
) ? error
.message
: error
)
91 var cbiKeyPairGenerate
= form
.DummyValue
.extend({
92 cfgvalue: function(section_id
, value
) {
95 'click': ui
.createHandlerFn(this, function(section_id
, ev
) {
96 var prv
= this.section
.getUIElement(section_id
, 'private_key'),
97 pub
= this.section
.getUIElement(section_id
, 'public_key'),
100 return generateKey().then(function(keypair
) {
101 prv
.setValue(keypair
.priv
);
102 pub
.setValue(keypair
.pub
);
103 map
.save(null, true);
106 }, [ _('Generate new key pair') ]);
110 function handleWindowDragDropIgnore(ev
) {
114 return network
.registerProtocol('wireguard', {
115 getI18n: function() {
116 return _('WireGuard VPN');
119 getIfname: function() {
120 return this._ubus('l3_device') || this.sid
;
123 getOpkgPackage: function() {
124 return 'wireguard-tools';
127 isFloating: function() {
131 isVirtual: function() {
135 getDevices: function() {
139 containsDevice: function(ifname
) {
140 return (network
.getIfnameOf(ifname
) == this.getIfname());
143 renderFormOptions: function(s
) {
146 // -- general ---------------------------------------------------------------------
148 o
= s
.taboption('general', form
.Value
, 'private_key', _('Private Key'), _('Required. Base64-encoded private key for this interface.'));
150 o
.validate
= validateBase64
;
153 var serverName
= this.getIfname();
155 o
= s
.taboption('general', form
.Value
, 'public_key', _('Public Key'), _('Base64-encoded public key of this interface for sharing.'));
157 o
.write = function() {/* write nothing */};
159 o
.load = function(section_id
) {
160 var privKey
= s
.formvalue(section_id
, 'private_key') || uci
.get('network', section_id
, 'private_key');
162 return getPublicAndPrivateKeyFromPrivate(privKey
).then(
164 return keypair
.pub
|| '';
167 return _('Error getting PublicKey');
171 s
.taboption('general', cbiKeyPairGenerate
, '_gen_server_keypair', ' ');
173 o
= s
.taboption('general', form
.Value
, 'listen_port', _('Listen Port'), _('Optional. UDP port used for outgoing and incoming packets.'));
175 o
.placeholder
= _('random');
178 o
= s
.taboption('general', form
.DynamicList
, 'addresses', _('IP Addresses'), _('Recommended. IP addresses of the WireGuard interface.'));
179 o
.datatype
= 'ipaddr';
182 o
= s
.taboption('general', form
.Flag
, 'nohostroute', _('No Host Routes'), _('Optional. Do not create host routes to peers.'));
185 o
= s
.taboption('general', form
.Button
, '_import', _('Import configuration'), _('Imports settings from an existing WireGuard configuration file'));
186 o
.inputtitle
= _('Load configuration…');
187 o
.onclick = function() {
188 return ss
.handleConfigImport('full');
191 // -- advanced --------------------------------------------------------------------
193 o
= s
.taboption('advanced', form
.Value
, 'mtu', _('MTU'), _('Optional. Maximum Transmission Unit of tunnel interface.'));
194 o
.datatype
= 'range(0,8940)';
195 o
.placeholder
= '1420';
198 o
= s
.taboption('advanced', form
.Value
, 'fwmark', _('Firewall Mark'), _('Optional. 32-bit mark for outgoing encrypted packets. Enter value in hex, starting with <code>0x</code>.'));
200 o
.validate = function(section_id
, value
) {
201 if (value
.length
> 0 && !value
.match(/^0x[a-fA-F0-9]{1,8}$/))
202 return _('Invalid hexadecimal value');
208 // -- peers -----------------------------------------------------------------------
211 s
.tab('peers', _('Peers'), _('Further information about WireGuard interfaces and peers at <a href=\'http://wireguard.com\'>wireguard.com</a>.'));
215 o
= s
.taboption('peers', form
.SectionValue
, '_peers', form
.GridSection
, 'wireguard_%s'.format(s
.section
));
216 o
.depends('proto', 'wireguard');
221 ss
.addbtntitle
= _('Add peer');
222 ss
.nodescriptions
= true;
223 ss
.modaltitle
= _('Edit peer');
225 ss
.handleDragConfig = function(ev
) {
226 ev
.stopPropagation();
228 ev
.dataTransfer
.dropEffect
= 'copy';
231 ss
.handleDropConfig = function(mode
, ev
) {
232 var file
= ev
.dataTransfer
.files
[0],
233 nodes
= ev
.currentTarget
,
234 input
= nodes
.querySelector('textarea'),
235 reader
= new FileReader();
238 reader
.onload = function(rev
) {
239 input
.value
= rev
.target
.result
.trim();
240 ss
.handleApplyConfig(mode
, nodes
, file
.name
, ev
);
243 reader
.readAsText(file
);
246 ev
.stopPropagation();
250 ss
.parseConfig = function(data
) {
251 var lines
= String(data
).split(/(\r?\n)+/),
253 config
= { peers
: [] },
256 for (var i
= 0; i
< lines
.length
; i
++) {
257 var line
= lines
[i
].replace(/#.*$/, '').trim();
259 if (line
.match(/^\[(\w+)\]$/)) {
260 section
= RegExp
.$1.toLowerCase();
262 if (section
== 'peer')
263 config
.peers
.push(s
= {});
267 else if (section
&& line
.match(/^(\w+)\s*=\s*(.+)$/)) {
269 val
= RegExp
.$2.trim();
272 s
[section
+ '_' + key
.toLowerCase()] = val
;
276 if (config
.interface_address
) {
277 config
.interface_address
= config
.interface_address
.split(/[, ]+/);
279 for (var i
= 0; i
< config
.interface_address
.length
; i
++)
280 if (!stubValidator
.apply('ipaddr', config
.interface_address
[i
]))
281 return _('Address setting is invalid');
284 if (config
.interface_dns
) {
285 config
.interface_dns
= config
.interface_dns
.split(/[, ]+/);
287 for (var i
= 0; i
< config
.interface_dns
.length
; i
++)
288 if (!stubValidator
.apply('ipaddr', config
.interface_dns
[i
], ['nomask']))
289 return _('DNS setting is invalid');
292 if (!config
.interface_privatekey
|| validateBase64(null, config
.interface_privatekey
) !== true)
293 return _('PrivateKey setting is missing or invalid');
295 if (!stubValidator
.apply('port', config
.interface_listenport
|| '0'))
296 return _('ListenPort setting is invalid');
298 for (var i
= 0; i
< config
.peers
.length
; i
++) {
299 var pconf
= config
.peers
[i
];
301 if (pconf
.peer_publickey
!= null && validateBase64(null, pconf
.peer_publickey
) !== true)
302 return _('PublicKey setting is invalid');
304 if (pconf
.peer_presharedkey
!= null && validateBase64(null, pconf
.peer_presharedkey
) !== true)
305 return _('PresharedKey setting is invalid');
307 if (pconf
.peer_allowedips
) {
308 pconf
.peer_allowedips
= pconf
.peer_allowedips
.split(/[, ]+/);
310 for (var j
= 0; j
< pconf
.peer_allowedips
.length
; j
++)
311 if (!stubValidator
.apply('ipaddr', pconf
.peer_allowedips
[j
]))
312 return _('AllowedIPs setting is invalid');
315 pconf
.peer_allowedips
= [ '0.0.0.0/0', '::/0' ];
318 if (pconf
.peer_endpoint
) {
319 var host_port
= pconf
.peer_endpoint
.match(/^\[([a-fA-F0-9:]+)\]:(\d+)$/) || pconf
.peer_endpoint
.match(/^(.+):(\d+)$/);
321 if (!host_port
|| !stubValidator
.apply('host', host_port
[1]) || !stubValidator
.apply('port', host_port
[2]))
322 return _('Endpoint setting is invalid');
324 pconf
.peer_endpoint
= [ host_port
[1], host_port
[2] ];
327 if (pconf
.peer_persistentkeepalive
== 'off' || pconf
.peer_persistentkeepalive
== '0')
328 delete pconf
.peer_persistentkeepalive
;
330 if (!stubValidator
.apply('port', pconf
.peer_persistentkeepalive
|| '0'))
331 return _('PersistentKeepAlive setting is invalid');
337 ss
.handleApplyConfig = function(mode
, nodes
, comment
, ev
) {
338 var input
= nodes
.querySelector('textarea').value
,
339 error
= nodes
.querySelector('.alert-message'),
340 cancel
= nodes
.nextElementSibling
.querySelector('.btn'),
341 config
= this.parseConfig(input
);
343 if (typeof(config
) == 'string') {
344 error
.firstChild
.data
= _('Cannot parse configuration: %s').format(config
);
345 error
.style
.display
= 'block';
349 if (mode
== 'full') {
350 var prv
= s
.formvalue(s
.section
, 'private_key');
352 if (prv
&& prv
!= config
.interface_privatekey
&& !confirm(_('Overwrite the current settings with the imported configuration?')))
355 return getPublicAndPrivateKeyFromPrivate(config
.interface_privatekey
).then(function(keypair
) {
356 s
.getOption('private_key').getUIElement(s
.section
).setValue(keypair
.priv
);
357 s
.getOption('public_key').getUIElement(s
.section
).setValue(keypair
.pub
);
358 s
.getOption('listen_port').getUIElement(s
.section
).setValue(config
.interface_listenport
|| '');
359 s
.getOption('addresses').getUIElement(s
.section
).setValue(config
.interface_address
);
361 if (config
.interface_dns
) {
362 s
.getOption('peerdns').getUIElement(s
.section
).setValue('0');
363 s
.getOption('dns').getUIElement(s
.section
).setValue(config
.interface_dns
);
366 for (var i
= 0; i
< config
.peers
.length
; i
++) {
367 var pconf
= config
.peers
[i
];
368 var sid
= uci
.add('network', 'wireguard_' + s
.section
);
370 uci
.sections('network', 'wireguard_' + s
.section
, function(peer
) {
371 if (peer
.public_key
== pconf
.peer_publickey
)
372 uci
.remove('network', peer
['.name']);
375 uci
.set('network', sid
, 'description', comment
|| _('Imported peer configuration'));
376 uci
.set('network', sid
, 'public_key', pconf
.peer_publickey
);
377 uci
.set('network', sid
, 'preshared_key', pconf
.peer_presharedkey
);
378 uci
.set('network', sid
, 'allowed_ips', pconf
.peer_allowedips
);
379 uci
.set('network', sid
, 'persistent_keepalive', pconf
.peer_persistentkeepalive
);
381 if (pconf
.peer_endpoint
) {
382 uci
.set('network', sid
, 'endpoint_host', pconf
.peer_endpoint
[0]);
383 uci
.set('network', sid
, 'endpoint_port', pconf
.peer_endpoint
[1]);
387 return s
.map
.save(null, true);
393 return getPublicAndPrivateKeyFromPrivate(config
.interface_privatekey
).then(function(keypair
) {
394 var sid
= uci
.add('network', 'wireguard_' + s
.section
);
395 var pub
= s
.formvalue(s
.section
, 'public_key');
397 uci
.sections('network', 'wireguard_' + s
.section
, function(peer
) {
398 if (peer
.public_key
== keypair
.pub
)
399 uci
.remove('network', peer
['.name']);
402 uci
.set('network', sid
, 'description', comment
|| _('Imported peer configuration'));
403 uci
.set('network', sid
, 'public_key', keypair
.pub
);
404 uci
.set('network', sid
, 'private_key', keypair
.priv
);
406 for (var i
= 0; i
< config
.peers
.length
; i
++) {
407 var pconf
= config
.peers
[i
];
409 if (pconf
.peer_publickey
== pub
) {
410 uci
.set('network', sid
, 'preshared_key', pconf
.peer_presharedkey
);
411 uci
.set('network', sid
, 'allowed_ips', pconf
.peer_allowedips
);
412 uci
.set('network', sid
, 'persistent_keepalive', pconf
.peer_persistentkeepalive
);
417 return s
.map
.save(null, true);
424 ss
.handleConfigImport = function(mode
) {
425 var mapNode
= ss
.getActiveModalMap(),
426 headNode
= mapNode
.parentNode
.querySelector('h4'),
429 var nodes
= E('div', {
430 'dragover': this.handleDragConfig
,
431 'drop': this.handleDropConfig
.bind(this, mode
)
433 E([], (mode
== 'full') ? [
434 E('p', _('Drag or paste a valid <em>*.conf</em> file below to configure the local WireGuard interface.'))
436 E('p', _('Paste or drag a WireGuard configuration (commonly <em>wg0.conf</em>) from another system below to create a matching peer entry allowing that system to connect to the local WireGuard interface.')),
437 E('p', _('To fully configure the local WireGuard interface from an existing (e.g. provider supplied) configuration file, use the <strong><a class="full-import" href="#">configuration import</a></strong> instead.'))
441 'placeholder': (mode
== 'full')
442 ? _('Paste or drag supplied WireGuard configuration file…')
443 : _('Paste or drag WireGuard peer configuration (wg0.conf) file…'),
444 'style': 'height:5em;width:100%; white-space:pre'
448 'class': 'alert-message',
449 'style': 'display:none'
453 var cancelFn = function() {
454 nodes
.parentNode
.removeChild(nodes
.nextSibling
);
455 nodes
.parentNode
.removeChild(nodes
);
456 mapNode
.classList
.remove('hidden');
457 mapNode
.nextSibling
.classList
.remove('hidden');
458 headNode
.removeChild(headNode
.lastChild
);
459 window
.removeEventListener('dragover', handleWindowDragDropIgnore
);
460 window
.removeEventListener('drop', handleWindowDragDropIgnore
);
463 var a
= nodes
.querySelector('a.full-import');
466 a
.addEventListener('click', ui
.createHandlerFn(this, function(mode
) {
468 this.handleConfigImport('full');
472 mapNode
.classList
.add('hidden');
473 mapNode
.nextElementSibling
.classList
.add('hidden');
475 headNode
.appendChild(E('span', [ ' » ', (mode
== 'full') ? _('Import configuration') : _('Import as peer') ]));
476 mapNode
.parentNode
.appendChild(E([], [
487 'class': 'btn primary',
488 'click': ui
.createHandlerFn(this, 'handleApplyConfig', mode
, nodes
, null)
489 }, [ _('Import settings') ])
493 window
.addEventListener('dragover', handleWindowDragDropIgnore
);
494 window
.addEventListener('drop', handleWindowDragDropIgnore
);
497 ss
.renderSectionAdd = function(/* ... */) {
498 var nodes
= this.super('renderSectionAdd', arguments
);
500 nodes
.appendChild(E('button', {
502 'click': ui
.createHandlerFn(this, 'handleConfigImport', 'peer')
503 }, [ _('Import configuration as peer…') ]));
508 ss
.renderSectionPlaceholder = function() {
509 return E('em', _('No peers defined yet.'));
512 o
= ss
.option(form
.Flag
, 'disabled', _('Peer disabled'), _('Enable / Disable peer. Restart wireguard interface to apply changes.'));
516 o
= ss
.option(form
.Value
, 'description', _('Description'), _('Optional. Description of peer.'));
517 o
.placeholder
= 'My Peer';
518 o
.datatype
= 'string';
521 o
.textvalue = function(section_id
) {
522 var dis
= ss
.getOption('disabled'),
523 pub
= ss
.getOption('public_key'),
524 prv
= ss
.getOption('private_key'),
525 psk
= ss
.getOption('preshared_key'),
526 name
= this.cfgvalue(section_id
),
527 key
= pub
.cfgvalue(section_id
);
531 name
? E('span', [ name
]) : E('em', [ _('Untitled peer') ])
535 if (dis
.cfgvalue(section_id
) == '1')
536 desc
.push(E('span', {
537 'class': 'ifacebadge',
538 'data-tooltip': _('WireGuard peer is disabled')
540 E('em', [ _('Disabled', 'Label indicating that WireGuard peer is disabled') ])
543 if (!key
|| !pub
.isValid(section_id
)) {
544 desc
.push(E('span', {
545 'class': 'ifacebadge',
546 'data-tooltip': _('Public key is missing')
548 E('em', [ _('Key missing', 'Label indicating that WireGuard peer lacks public key') ])
554 'class': 'ifacebadge',
555 'data-tooltip': _('Public key: %h', 'Tooltip displaying full WireGuard peer public key').format(key
)
557 E('code', [ key
.replace(/^(.{5}).+(.{6})$/, '$1…$2') ])
560 (prv
.cfgvalue(section_id
) && prv
.isValid(section_id
))
562 'class': 'ifacebadge',
563 'data-tooltip': _('Private key present')
564 }, [ _('Private', 'Label indicating that WireGuard peer private key is stored') ]) : '',
566 (psk
.cfgvalue(section_id
) && psk
.isValid(section_id
))
568 'class': 'ifacebadge',
569 'data-tooltip': _('Preshared key in use')
570 }, [ _('PSK', 'Label indicating that WireGuard peer uses a PSK') ]) : ''
577 function handleKeyChange(ev
, section_id
, value
) {
578 var prv
= this.section
.getUIElement(section_id
, 'private_key'),
579 btn
= this.map
.findElement('.btn.qr-code');
581 btn
.disabled
= (!prv
.isValid() || !prv
.getValue());
584 o
= ss
.option(form
.Value
, 'public_key', _('Public Key'), _('Required. Public key of the WireGuard peer.'));
586 o
.validate
= validateBase64
;
587 o
.onchange
= handleKeyChange
;
589 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.'));
591 o
.validate
= validateBase64
;
592 o
.onchange
= handleKeyChange
;
595 o
= ss
.option(cbiKeyPairGenerate
, '_gen_peer_keypair', ' ');
598 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.'));
600 o
.validate
= validateBase64
;
603 o
= ss
.option(form
.DummyValue
, '_gen_psk', ' ');
605 o
.cfgvalue = function(section_id
, value
) {
608 'click': ui
.createHandlerFn(this, function(section_id
, ev
) {
609 var psk
= this.section
.getUIElement(section_id
, 'preshared_key'),
612 return generatePsk().then(function(key
) {
614 map
.save(null, true);
617 }, [ _('Generate preshared key') ]);
620 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."));
621 o
.datatype
= 'ipaddr';
622 o
.textvalue = function(section_id
) {
623 var ips
= L
.toArray(this.cfgvalue(section_id
)),
626 for (var i
= 0; i
< ips
.length
; i
++) {
629 'class': 'ifacebadge cbi-tooltip-container'
631 _('+ %d more', 'Label indicating further amount of allowed ips').format(ips
.length
- i
),
633 'class': 'cbi-tooltip'
635 E('ul', ips
.map(function(ip
) {
637 E('span', { 'class': 'ifacebadge' }, [ ip
])
646 list
.push(E('span', { 'class': 'ifacebadge' }, [ ips
[i
] ]));
652 return E('span', { 'style': 'display:inline-flex;flex-wrap:wrap;gap:.125em' }, list
);
655 o
= ss
.option(form
.Flag
, 'route_allowed_ips', _('Route Allowed IPs'), _('Optional. Create routes for Allowed IPs for this peer.'));
658 o
= ss
.option(form
.Value
, 'endpoint_host', _('Endpoint Host'), _('Optional. Host of peer. Names are resolved prior to bringing up the interface.'));
659 o
.placeholder
= 'vpn.example.com';
661 o
.textvalue = function(section_id
) {
662 var host
= this.cfgvalue(section_id
),
663 port
= this.section
.cfgvalue(section_id
, 'endpoint_port');
665 return (host
&& port
)
666 ? '%h:%d'.format(host
, port
)
668 ? '%h:*'.format(host
)
670 ? '*:%d'.format(port
)
674 o
= ss
.option(form
.Value
, 'endpoint_port', _('Endpoint Port'), _('Optional. Port of peer.'));
676 o
.placeholder
= '51820';
679 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.'));
681 o
.datatype
= 'range(0,65535)';
686 o
= ss
.option(form
.DummyValue
, '_keyops', _('Configuration Export'),
687 _('Generates a configuration suitable for import on a WireGuard peer'));
691 o
.createPeerConfig = function(section_id
, endpoint
, ips
, eips
, dns
) {
692 var pub
= s
.formvalue(s
.section
, 'public_key'),
693 port
= s
.formvalue(s
.section
, 'listen_port') || '51820',
694 prv
= this.section
.formvalue(section_id
, 'private_key'),
695 psk
= this.section
.formvalue(section_id
, 'preshared_key'),
696 eport
= this.section
.formvalue(section_id
, 'endpoint_port'),
697 keep
= this.section
.formvalue(section_id
, 'persistent_keepalive');
701 'PrivateKey = ' + prv
,
702 eips
&& eips
.length
? 'Address = ' + eips
.join(', ') : '# Address not defined',
703 eport
? 'ListenPort = ' + eport
: '# ListenPort not defined',
704 dns
&& dns
.length
? 'DNS = ' + dns
.join(', ') : '# DNS not defined',
707 'PublicKey = ' + pub
,
708 psk
? 'PresharedKey = ' + psk
: '# PresharedKey not used',
709 ips
&& ips
.length
? 'AllowedIPs = ' + ips
.join(', ') : '# AllowedIPs not defined',
710 endpoint
? 'Endpoint = ' + endpoint
+ ':' + port
: '# Endpoint not defined',
711 keep
? 'PersistentKeepAlive = ' + keep
: '# PersistentKeepAlive not defined'
715 o
.handleGenerateQR = function(section_id
, ev
) {
716 var mapNode
= ss
.getActiveModalMap(),
717 headNode
= mapNode
.parentNode
.querySelector('h4'),
718 configGenerator
= this.createPeerConfig
.bind(this, section_id
),
720 eips
= this.section
.formvalue(section_id
, 'allowed_ips');
723 network
.getWANNetworks(),
724 network
.getWAN6Networks(),
725 network
.getNetwork('lan'),
726 L
.resolveDefault(uci
.load('ddns')),
727 L
.resolveDefault(uci
.load('system')),
728 parent
.save(null, true)
729 ]).then(function(data
) {
732 uci
.sections('ddns', 'service', function(s
) {
733 if (typeof(s
.lookup_host
) == 'string' && s
.enabled
== '1')
734 hostnames
.push(s
.lookup_host
);
737 uci
.sections('system', 'system', function(s
) {
738 if (typeof(s
.hostname
) == 'string' && s
.hostname
.indexOf('.') > 0)
739 hostnames
.push(s
.hostname
);
742 for (var i
= 0; i
< data
[0].length
; i
++)
743 hostnames
.push
.apply(hostnames
, data
[0][i
].getIPAddrs().map(function(ip
) { return ip
.split('/')[0] }));
745 for (var i
= 0; i
< data
[1].length
; i
++)
746 hostnames
.push
.apply(hostnames
, data
[1][i
].getIP6Addrs().map(function(ip
) { return ip
.split('/')[0] }));
748 var ips
= [ '0.0.0.0/0', '::/0' ];
754 var lanIp
= lan
.getIPAddr();
762 qrm
= new form
.JSONMap({ config
: { endpoint
: hostnames
[0], allowed_ips
: ips
, addresses
: eips
, dns_servers
: dns
} }, null, _('The generated configuration can be imported into a WireGuard client application to set up a connection towards this device.'));
765 qrs
= qrm
.section(form
.NamedSection
, 'config');
767 function handleConfigChange(ev
, section_id
, value
) {
768 var code
= this.map
.findElement('.qr-code'),
769 conf
= this.map
.findElement('.client-config'),
770 endpoint
= this.section
.getUIElement(section_id
, 'endpoint'),
771 ips
= this.section
.getUIElement(section_id
, 'allowed_ips');
772 eips
= this.section
.getUIElement(section_id
, 'addresses');
773 dns
= this.section
.getUIElement(section_id
, 'dns_servers');
775 if (this.isValid(section_id
)) {
776 conf
.firstChild
.data
= configGenerator(endpoint
.getValue(), ips
.getValue(), eips
.getValue(), dns
.getValue());
777 code
.style
.opacity
= '.5';
779 invokeQREncode(conf
.firstChild
.data
, code
);
783 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.'));
784 qro
.datatype
= 'or(ipaddr,hostname)';
785 hostnames
.forEach(function(hostname
) { qro
.value(hostname
) });
786 qro
.onchange
= handleConfigChange
;
788 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.'));
789 qro
.datatype
= 'ipaddr';
791 ips
.forEach(function(ip
) { qro
.value(ip
) });
792 qro
.onchange
= handleConfigChange
;
794 qro
= qrs
.option(form
.DynamicList
, 'dns_servers', _('DNS Servers'), _('DNS servers for the remote clients using this tunnel to your openwrt device. Some wireguard clients require this to be set.'));
795 qro
.datatype
= 'ipaddr';
797 qro
.onchange
= handleConfigChange
;
799 qro
= qrs
.option(form
.DynamicList
, 'addresses', _('Addresses'), _('IP addresses for the peer to use inside the tunnel. Some clients require this setting.'));
800 qro
.datatype
= 'ipaddr';
802 eips
.forEach(function(eip
) { qro
.value(eip
) });
803 qro
.onchange
= handleConfigChange
;
805 qro
= qrs
.option(form
.DummyValue
, 'output');
806 qro
.renderWidget = function() {
807 var peer_config
= configGenerator(hostnames
[0], ips
, eips
, dns
);
809 var node
= E('div', {
810 'style': 'display:flex;flex-wrap:wrap;align-items:center;gap:.5em;width:100%'
814 'style': 'width:320px;flex:0 1 320px;text-align:center'
816 E('em', { 'class': 'spinning' }, [ _('Generating QR code…') ])
819 'class': 'client-config',
820 'style': 'flex:1;white-space:pre;overflow:auto',
821 'click': function(ev
) {
822 var sel
= window
.getSelection(),
823 range
= document
.createRange();
825 range
.selectNodeContents(ev
.currentTarget
);
827 sel
.removeAllRanges();
833 invokeQREncode(peer_config
, node
.firstChild
);
838 return qrm
.render().then(function(nodes
) {
839 mapNode
.classList
.add('hidden');
840 mapNode
.nextElementSibling
.classList
.add('hidden');
842 headNode
.appendChild(E('span', [ ' » ', _('Generate configuration') ]));
843 mapNode
.parentNode
.appendChild(E([], [
850 'click': function() {
851 nodes
.parentNode
.removeChild(nodes
.nextSibling
);
852 nodes
.parentNode
.removeChild(nodes
);
853 mapNode
.classList
.remove('hidden');
854 mapNode
.nextSibling
.classList
.remove('hidden');
855 headNode
.removeChild(headNode
.lastChild
);
857 }, [ _('Back to peer configuration') ])
861 if (!s
.formvalue(s
.section
, 'listen_port')) {
862 nodes
.appendChild(E('div', { 'class': 'alert-message' }, [
864 _('No fixed interface listening port defined, peers might not be able to initiate connections to this WireGuard instance!')
872 o
.cfgvalue = function(section_id
, value
) {
873 var privkey
= this.section
.cfgvalue(section_id
, 'private_key');
876 'class': 'btn qr-code',
877 'style': 'display:inline-flex;align-items:center;gap:.5em',
878 'click': ui
.createHandlerFn(this, 'handleGenerateQR', section_id
),
879 'disabled': privkey
? null : ''
881 Object
.assign(E(qrIcon
), { style
: 'width:22px;height:22px' }),
882 _('Generate configuration…')
887 deleteConfiguration: function() {
888 uci
.sections('network', 'wireguard_%s'.format(this.sid
), function(s
) {
889 uci
.remove('network', s
['.name']);