d05acfbe79cbdae5842062d72f0da85f8a2e21f3
[project/luci.git] / protocols / luci-proto-wireguard / htdocs / luci-static / resources / protocol / wireguard.js
1 'use strict';
2 'require fs';
3 'require ui';
4 'require dom';
5 'require uci';
6 'require rpc';
7 'require form';
8 'require network';
9 'require validation';
10
11 var generateKey = rpc.declare({
12 object: 'luci.wireguard',
13 method: 'generateKeyPair',
14 expect: { keys: {} }
15 });
16
17 var getPublicAndPrivateKeyFromPrivate = rpc.declare({
18 object: 'luci.wireguard',
19 method: 'getPublicAndPrivateKeyFromPrivate',
20 params: ['privkey'],
21 expect: { keys: {} }
22 });
23
24 var generatePsk = rpc.declare({
25 object: 'luci.wireguard',
26 method: 'generatePsk',
27 expect: { psk: '' }
28 });
29
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>';
31
32 function validateBase64(section_id, value) {
33 if (value.length == 0)
34 return true;
35
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');
38
39 if (value[43] != "=" )
40 return _('Invalid Base64 key string');
41
42 return true;
43 }
44
45 var stubValidator = {
46 factory: validation,
47 apply: function(type, value, args) {
48 if (value != null)
49 this.value = value;
50
51 return validation.types[type].apply(this, args);
52 },
53 assert: function(condition) {
54 return !!condition;
55 }
56 };
57
58 function generateDescription(name, texts) {
59 return E('li', { 'style': 'color: inherit;' }, [
60 E('span', name),
61 E('ul', texts.map(function (text) {
62 return E('li', { 'style': 'color: inherit;' }, text);
63 }))
64 ]);
65 }
66
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 = '';
76
77 if (L.isObject(error) && error.name == 'NotFoundError') {
78 dom.content(code, [
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.'))
81 ]);
82 }
83 else {
84 dom.content(code, [
85 _('Unable to generate QR code: %s').format(L.isObject(error) ? error.message : error)
86 ]);
87 }
88 });
89 }
90
91 var cbiKeyPairGenerate = form.DummyValue.extend({
92 cfgvalue: function(section_id, value) {
93 return E('button', {
94 'class': 'btn',
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'),
98 map = this.map;
99
100 return generateKey().then(function(keypair) {
101 prv.setValue(keypair.priv);
102 pub.setValue(keypair.pub);
103 map.save(null, true);
104 });
105 }, section_id)
106 }, [ _('Generate new key pair') ]);
107 }
108 });
109
110 function handleWindowDragDropIgnore(ev) {
111 ev.preventDefault()
112 }
113
114 return network.registerProtocol('wireguard', {
115 getI18n: function() {
116 return _('WireGuard VPN');
117 },
118
119 getIfname: function() {
120 return this._ubus('l3_device') || this.sid;
121 },
122
123 getOpkgPackage: function() {
124 return 'wireguard-tools';
125 },
126
127 isFloating: function() {
128 return true;
129 },
130
131 isVirtual: function() {
132 return true;
133 },
134
135 getDevices: function() {
136 return null;
137 },
138
139 containsDevice: function(ifname) {
140 return (network.getIfnameOf(ifname) == this.getIfname());
141 },
142
143 renderFormOptions: function(s) {
144 var o, ss, ss2;
145
146 // -- general ---------------------------------------------------------------------
147
148 o = s.taboption('general', form.Value, 'private_key', _('Private Key'), _('Required. Base64-encoded private key for this interface.'));
149 o.password = true;
150 o.validate = validateBase64;
151 o.rmempty = false;
152
153 var serverName = this.getIfname();
154
155 o = s.taboption('general', form.Value, 'public_key', _('Public Key'), _('Base64-encoded public key of this interface for sharing.'));
156 o.rmempty = false;
157 o.write = function() {/* write nothing */};
158
159 o.load = function(section_id) {
160 var privKey = s.formvalue(section_id, 'private_key') || uci.get('network', section_id, 'private_key');
161
162 return getPublicAndPrivateKeyFromPrivate(privKey).then(
163 function(keypair) {
164 return keypair.pub || '';
165 },
166 function(error) {
167 return _('Error getting PublicKey');
168 }, this)
169 };
170
171 s.taboption('general', cbiKeyPairGenerate, '_gen_server_keypair', ' ');
172
173 o = s.taboption('general', form.Value, 'listen_port', _('Listen Port'), _('Optional. UDP port used for outgoing and incoming packets.'));
174 o.datatype = 'port';
175 o.placeholder = _('random');
176 o.optional = true;
177
178 o = s.taboption('general', form.DynamicList, 'addresses', _('IP Addresses'), _('Recommended. IP addresses of the WireGuard interface.'));
179 o.datatype = 'ipaddr';
180 o.optional = true;
181
182 o = s.taboption('general', form.Flag, 'nohostroute', _('No Host Routes'), _('Optional. Do not create host routes to peers.'));
183 o.optional = true;
184
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');
189 };
190
191 // -- advanced --------------------------------------------------------------------
192
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';
196 o.optional = true;
197
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>.'));
199 o.optional = true;
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');
203
204 return true;
205 };
206
207
208 // -- peers -----------------------------------------------------------------------
209
210 try {
211 s.tab('peers', _('Peers'), _('Further information about WireGuard interfaces and peers at <a href=\'http://wireguard.com\'>wireguard.com</a>.'));
212 }
213 catch(e) {}
214
215 o = s.taboption('peers', form.SectionValue, '_peers', form.GridSection, 'wireguard_%s'.format(s.section));
216 o.depends('proto', 'wireguard');
217
218 ss = o.subsection;
219 ss.anonymous = true;
220 ss.addremove = true;
221 ss.addbtntitle = _('Add peer');
222 ss.nodescriptions = true;
223 ss.modaltitle = _('Edit peer');
224
225 ss.handleDragConfig = function(ev) {
226 ev.stopPropagation();
227 ev.preventDefault();
228 ev.dataTransfer.dropEffect = 'copy';
229 };
230
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();
236
237 if (file) {
238 reader.onload = function(rev) {
239 input.value = rev.target.result.trim();
240 ss.handleApplyConfig(mode, nodes, file.name, ev);
241 };
242
243 reader.readAsText(file);
244 }
245
246 ev.stopPropagation();
247 ev.preventDefault();
248 };
249
250 ss.parseConfig = function(data) {
251 var lines = String(data).split(/(\r?\n)+/),
252 section = null,
253 config = { peers: [] },
254 s;
255
256 for (var i = 0; i < lines.length; i++) {
257 var line = lines[i].replace(/#.*$/, '').trim();
258
259 if (line.match(/^\[(\w+)\]$/)) {
260 section = RegExp.$1.toLowerCase();
261
262 if (section == 'peer')
263 config.peers.push(s = {});
264 else
265 s = config;
266 }
267 else if (section && line.match(/^(\w+)\s*=\s*(.+)$/)) {
268 var key = RegExp.$1,
269 val = RegExp.$2.trim();
270
271 if (val.length)
272 s[section + '_' + key.toLowerCase()] = val;
273 }
274 }
275
276 if (config.interface_address) {
277 config.interface_address = config.interface_address.split(/[, ]+/);
278
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');
282 }
283
284 if (config.interface_dns) {
285 config.interface_dns = config.interface_dns.split(/[, ]+/);
286
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');
290 }
291
292 if (!config.interface_privatekey || validateBase64(null, config.interface_privatekey) !== true)
293 return _('PrivateKey setting is missing or invalid');
294
295 if (!stubValidator.apply('port', config.interface_listenport || '0'))
296 return _('ListenPort setting is invalid');
297
298 for (var i = 0; i < config.peers.length; i++) {
299 var pconf = config.peers[i];
300
301 if (pconf.peer_publickey != null && validateBase64(null, pconf.peer_publickey) !== true)
302 return _('PublicKey setting is invalid');
303
304 if (pconf.peer_presharedkey != null && validateBase64(null, pconf.peer_presharedkey) !== true)
305 return _('PresharedKey setting is invalid');
306
307 if (pconf.peer_allowedips) {
308 pconf.peer_allowedips = pconf.peer_allowedips.split(/[, ]+/);
309
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');
313 }
314 else {
315 pconf.peer_allowedips = [ '0.0.0.0/0', '::/0' ];
316 }
317
318 if (pconf.peer_endpoint) {
319 var host_port = pconf.peer_endpoint.match(/^\[([a-fA-F0-9:]+)\]:(\d+)$/) || pconf.peer_endpoint.match(/^(.+):(\d+)$/);
320
321 if (!host_port || !stubValidator.apply('host', host_port[1]) || !stubValidator.apply('port', host_port[2]))
322 return _('Endpoint setting is invalid');
323
324 pconf.peer_endpoint = [ host_port[1], host_port[2] ];
325 }
326
327 if (pconf.peer_persistentkeepalive == 'off' || pconf.peer_persistentkeepalive == '0')
328 delete pconf.peer_persistentkeepalive;
329
330 if (!stubValidator.apply('port', pconf.peer_persistentkeepalive || '0'))
331 return _('PersistentKeepAlive setting is invalid');
332 }
333
334 return config;
335 };
336
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);
342
343 if (typeof(config) == 'string') {
344 error.firstChild.data = _('Cannot parse configuration: %s').format(config);
345 error.style.display = 'block';
346 return;
347 }
348
349 if (mode == 'full') {
350 var prv = s.formvalue(s.section, 'private_key');
351
352 if (prv && prv != config.interface_privatekey && !confirm(_('Overwrite the current settings with the imported configuration?')))
353 return;
354
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);
360
361 if (config.interface_dns)
362 s.getOption('dns').getUIElement(s.section).setValue(config.interface_dns);
363
364 for (var i = 0; i < config.peers.length; i++) {
365 var pconf = config.peers[i];
366 var sid = uci.add('network', 'wireguard_' + s.section);
367
368 uci.sections('network', 'wireguard_' + s.section, function(peer) {
369 if (peer.public_key == pconf.peer_publickey)
370 uci.remove('network', peer['.name']);
371 });
372
373 uci.set('network', sid, 'description', comment || _('Imported peer configuration'));
374 uci.set('network', sid, 'public_key', pconf.peer_publickey);
375 uci.set('network', sid, 'preshared_key', pconf.peer_presharedkey);
376 uci.set('network', sid, 'allowed_ips', pconf.peer_allowedips);
377 uci.set('network', sid, 'persistent_keepalive', pconf.peer_persistentkeepalive);
378
379 if (pconf.peer_endpoint) {
380 uci.set('network', sid, 'endpoint_host', pconf.peer_endpoint[0]);
381 uci.set('network', sid, 'endpoint_port', pconf.peer_endpoint[1]);
382 }
383 }
384
385 return s.map.save(null, true);
386 }).then(function() {
387 cancel.click();
388 });
389 }
390 else {
391 return getPublicAndPrivateKeyFromPrivate(config.interface_privatekey).then(function(keypair) {
392 var sid = uci.add('network', 'wireguard_' + s.section);
393 var pub = s.formvalue(s.section, 'public_key');
394
395 uci.sections('network', 'wireguard_' + s.section, function(peer) {
396 if (peer.public_key == keypair.pub)
397 uci.remove('network', peer['.name']);
398 });
399
400 uci.set('network', sid, 'description', comment || _('Imported peer configuration'));
401 uci.set('network', sid, 'public_key', keypair.pub);
402 uci.set('network', sid, 'private_key', keypair.priv);
403
404 for (var i = 0; i < config.peers.length; i++) {
405 var pconf = config.peers[i];
406
407 if (pconf.peer_publickey == pub) {
408 uci.set('network', sid, 'preshared_key', pconf.peer_presharedkey);
409 uci.set('network', sid, 'allowed_ips', pconf.peer_allowedips);
410 uci.set('network', sid, 'persistent_keepalive', pconf.peer_persistentkeepalive);
411 break;
412 }
413 }
414
415 return s.map.save(null, true);
416 }).then(function() {
417 cancel.click();
418 });
419 }
420 };
421
422 ss.handleConfigImport = function(mode) {
423 var mapNode = ss.getActiveModalMap(),
424 headNode = mapNode.parentNode.querySelector('h4'),
425 parent = this.map;
426
427 var nodes = E('div', {
428 'dragover': this.handleDragConfig,
429 'drop': this.handleDropConfig.bind(this, mode)
430 }, [
431 E([], (mode == 'full') ? [
432 E('p', _('Drag or paste a valid <em>*.conf</em> file below to configure the local WireGuard interface.'))
433 ] : [
434 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.')),
435 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.'))
436 ]),
437 E('p', [
438 E('textarea', {
439 'placeholder': (mode == 'full')
440 ? _('Paste or drag supplied WireGuard configuration file…')
441 : _('Paste or drag WireGuard peer configuration (wg0.conf) file…'),
442 'style': 'height:5em;width:100%; white-space:pre'
443 })
444 ]),
445 E('div', {
446 'class': 'alert-message',
447 'style': 'display:none'
448 }, [''])
449 ]);
450
451 var cancelFn = function() {
452 nodes.parentNode.removeChild(nodes.nextSibling);
453 nodes.parentNode.removeChild(nodes);
454 mapNode.classList.remove('hidden');
455 mapNode.nextSibling.classList.remove('hidden');
456 headNode.removeChild(headNode.lastChild);
457 window.removeEventListener('dragover', handleWindowDragDropIgnore);
458 window.removeEventListener('drop', handleWindowDragDropIgnore);
459 };
460
461 var a = nodes.querySelector('a.full-import');
462
463 if (a) {
464 a.addEventListener('click', ui.createHandlerFn(this, function(mode) {
465 cancelFn();
466 this.handleConfigImport('full');
467 }));
468 }
469
470 mapNode.classList.add('hidden');
471 mapNode.nextElementSibling.classList.add('hidden');
472
473 headNode.appendChild(E('span', [ ' » ', (mode == 'full') ? _('Import configuration') : _('Import as peer') ]));
474 mapNode.parentNode.appendChild(E([], [
475 nodes,
476 E('div', {
477 'class': 'right'
478 }, [
479 E('button', {
480 'class': 'btn',
481 'click': cancelFn
482 }, [ _('Cancel') ]),
483 ' ',
484 E('button', {
485 'class': 'btn primary',
486 'click': ui.createHandlerFn(this, 'handleApplyConfig', mode, nodes, null)
487 }, [ _('Import settings') ])
488 ])
489 ]));
490
491 window.addEventListener('dragover', handleWindowDragDropIgnore);
492 window.addEventListener('drop', handleWindowDragDropIgnore);
493 };
494
495 ss.renderSectionAdd = function(/* ... */) {
496 var nodes = this.super('renderSectionAdd', arguments);
497
498 nodes.appendChild(E('button', {
499 'class': 'btn',
500 'click': ui.createHandlerFn(this, 'handleConfigImport', 'peer')
501 }, [ _('Import configuration as peer…') ]));
502
503 return nodes;
504 };
505
506 ss.renderSectionPlaceholder = function() {
507 return E('em', _('No peers defined yet.'));
508 };
509
510 o = ss.option(form.Flag, 'disabled', _('Peer disabled'), _('Enable / Disable peer. Restart wireguard interface to apply changes.'));
511 o.modalonly = true;
512 o.optional = true;
513
514 o = ss.option(form.Value, 'description', _('Description'), _('Optional. Description of peer.'));
515 o.placeholder = 'My Peer';
516 o.datatype = 'string';
517 o.optional = true;
518 o.width = '30%';
519 o.textvalue = function(section_id) {
520 var dis = ss.getOption('disabled'),
521 pub = ss.getOption('public_key'),
522 prv = ss.getOption('private_key'),
523 psk = ss.getOption('preshared_key'),
524 name = this.cfgvalue(section_id),
525 key = pub.cfgvalue(section_id);
526
527 var desc = [
528 E('p', [
529 name ? E('span', [ name ]) : E('em', [ _('Untitled peer') ])
530 ])
531 ];
532
533 if (dis.cfgvalue(section_id) == '1')
534 desc.push(E('span', {
535 'class': 'ifacebadge',
536 'data-tooltip': _('WireGuard peer is disabled')
537 }, [
538 E('em', [ _('Disabled', 'Label indicating that WireGuard peer is disabled') ])
539 ]), ' ');
540
541 if (!key || !pub.isValid(section_id)) {
542 desc.push(E('span', {
543 'class': 'ifacebadge',
544 'data-tooltip': _('Public key is missing')
545 }, [
546 E('em', [ _('Key missing', 'Label indicating that WireGuard peer lacks public key') ])
547 ]));
548 }
549 else {
550 desc.push(
551 E('span', {
552 'class': 'ifacebadge',
553 'data-tooltip': _('Public key: %h', 'Tooltip displaying full WireGuard peer public key').format(key)
554 }, [
555 E('code', [ key.replace(/^(.{5}).+(.{6})$/, '$1…$2') ])
556 ]),
557 ' ',
558 (prv.cfgvalue(section_id) && prv.isValid(section_id))
559 ? E('span', {
560 'class': 'ifacebadge',
561 'data-tooltip': _('Private key present')
562 }, [ _('Private', 'Label indicating that WireGuard peer private key is stored') ]) : '',
563 ' ',
564 (psk.cfgvalue(section_id) && psk.isValid(section_id))
565 ? E('span', {
566 'class': 'ifacebadge',
567 'data-tooltip': _('Preshared key in use')
568 }, [ _('PSK', 'Label indicating that WireGuard peer uses a PSK') ]) : ''
569 );
570 }
571
572 return E([], desc);
573 };
574
575 function handleKeyChange(ev, section_id, value) {
576 var prv = this.section.getUIElement(section_id, 'private_key'),
577 btn = this.map.findElement('.btn.qr-code');
578
579 btn.disabled = (!prv.isValid() || !prv.getValue());
580 }
581
582 o = ss.option(form.Value, 'public_key', _('Public Key'), _('Required. Public key of the WireGuard peer.'));
583 o.modalonly = true;
584 o.validate = validateBase64;
585 o.onchange = handleKeyChange;
586
587 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.'));
588 o.modalonly = true;
589 o.validate = validateBase64;
590 o.onchange = handleKeyChange;
591 o.password = true;
592
593 o = ss.option(cbiKeyPairGenerate, '_gen_peer_keypair', ' ');
594 o.modalonly = true;
595
596 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.'));
597 o.modalonly = true;
598 o.validate = validateBase64;
599 o.password = true;
600
601 o = ss.option(form.DummyValue, '_gen_psk', ' ');
602 o.modalonly = true;
603 o.cfgvalue = function(section_id, value) {
604 return E('button', {
605 'class': 'btn',
606 'click': ui.createHandlerFn(this, function(section_id, ev) {
607 var psk = this.section.getUIElement(section_id, 'preshared_key'),
608 map = this.map;
609
610 return generatePsk().then(function(key) {
611 psk.setValue(key);
612 map.save(null, true);
613 });
614 }, section_id)
615 }, [ _('Generate preshared key') ]);
616 };
617
618 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."));
619 o.datatype = 'ipaddr';
620 o.textvalue = function(section_id) {
621 var ips = L.toArray(this.cfgvalue(section_id)),
622 list = [];
623
624 for (var i = 0; i < ips.length; i++) {
625 if (i > 7) {
626 list.push(E('em', {
627 'class': 'ifacebadge cbi-tooltip-container'
628 }, [
629 _('+ %d more', 'Label indicating further amount of allowed ips').format(ips.length - i),
630 E('span', {
631 'class': 'cbi-tooltip'
632 }, [
633 E('ul', ips.map(function(ip) {
634 return E('li', [
635 E('span', { 'class': 'ifacebadge' }, [ ip ])
636 ]);
637 }))
638 ])
639 ]));
640
641 break;
642 }
643
644 list.push(E('span', { 'class': 'ifacebadge' }, [ ips[i] ]));
645 }
646
647 if (!list.length)
648 list.push('*');
649
650 return E('span', { 'style': 'display:inline-flex;flex-wrap:wrap;gap:.125em' }, list);
651 };
652
653 o = ss.option(form.Flag, 'route_allowed_ips', _('Route Allowed IPs'), _('Optional. Create routes for Allowed IPs for this peer.'));
654 o.modalonly = true;
655
656 o = ss.option(form.Value, 'endpoint_host', _('Endpoint Host'), _('Optional. Host of peer. Names are resolved prior to bringing up the interface.'));
657 o.placeholder = 'vpn.example.com';
658 o.datatype = 'host';
659 o.textvalue = function(section_id) {
660 var host = this.cfgvalue(section_id),
661 port = this.section.cfgvalue(section_id, 'endpoint_port');
662
663 return (host && port)
664 ? '%h:%d'.format(host, port)
665 : (host
666 ? '%h:*'.format(host)
667 : (port
668 ? '*:%d'.format(port)
669 : '*'));
670 };
671
672 o = ss.option(form.Value, 'endpoint_port', _('Endpoint Port'), _('Optional. Port of peer.'));
673 o.modalonly = true;
674 o.placeholder = '51820';
675 o.datatype = 'port';
676
677 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.'));
678 o.modalonly = true;
679 o.datatype = 'range(0,65535)';
680 o.placeholder = '0';
681
682
683
684 o = ss.option(form.DummyValue, '_keyops', _('Configuration Export'),
685 _('Generates a configuration suitable for import on a WireGuard peer'));
686
687 o.modalonly = true;
688
689 o.createPeerConfig = function(section_id, endpoint, ips, eips, dns) {
690 var pub = s.formvalue(s.section, 'public_key'),
691 port = s.formvalue(s.section, 'listen_port') || '51820',
692 prv = this.section.formvalue(section_id, 'private_key'),
693 psk = this.section.formvalue(section_id, 'preshared_key'),
694 eport = this.section.formvalue(section_id, 'endpoint_port'),
695 keep = this.section.formvalue(section_id, 'persistent_keepalive');
696
697 // If endpoint is IPv6 we must escape it with []
698 if (endpoint.indexOf(':') > 0) {
699 endpoint = '['+endpoint+']';
700 }
701
702 return [
703 '[Interface]',
704 'PrivateKey = ' + prv,
705 eips && eips.length ? 'Address = ' + eips.join(', ') : '# Address not defined',
706 eport ? 'ListenPort = ' + eport : '# ListenPort not defined',
707 dns && dns.length ? 'DNS = ' + dns.join(', ') : '# DNS not defined',
708 '',
709 '[Peer]',
710 'PublicKey = ' + pub,
711 psk ? 'PresharedKey = ' + psk : '# PresharedKey not used',
712 ips && ips.length ? 'AllowedIPs = ' + ips.join(', ') : '# AllowedIPs not defined',
713 endpoint ? 'Endpoint = ' + endpoint + ':' + port : '# Endpoint not defined',
714 keep ? 'PersistentKeepAlive = ' + keep : '# PersistentKeepAlive not defined'
715 ].join('\n');
716 };
717
718 o.handleGenerateQR = function(section_id, ev) {
719 var mapNode = ss.getActiveModalMap(),
720 headNode = mapNode.parentNode.querySelector('h4'),
721 configGenerator = this.createPeerConfig.bind(this, section_id),
722 parent = this.map,
723 eips = this.section.formvalue(section_id, 'allowed_ips');
724
725 return Promise.all([
726 network.getWANNetworks(),
727 network.getWAN6Networks(),
728 network.getNetwork('lan'),
729 L.resolveDefault(uci.load('ddns')),
730 L.resolveDefault(uci.load('system')),
731 parent.save(null, true)
732 ]).then(function(data) {
733 var hostnames = [];
734
735 uci.sections('ddns', 'service', function(s) {
736 if (typeof(s.lookup_host) == 'string' && s.enabled == '1')
737 hostnames.push(s.lookup_host);
738 });
739
740 uci.sections('system', 'system', function(s) {
741 if (typeof(s.hostname) == 'string' && s.hostname.indexOf('.') > 0)
742 hostnames.push(s.hostname);
743 });
744
745 for (var i = 0; i < data[0].length; i++)
746 hostnames.push.apply(hostnames, data[0][i].getIPAddrs().map(function(ip) { return ip.split('/')[0] }));
747
748 for (var i = 0; i < data[1].length; i++)
749 hostnames.push.apply(hostnames, data[1][i].getIP6Addrs().map(function(ip) { return ip.split('/')[0] }));
750
751 var ips = [ '0.0.0.0/0', '::/0' ];
752
753 var dns = [];
754
755 var lan = data[2];
756 if (lan) {
757 var lanIp = lan.getIPAddr();
758 if (lanIp) {
759 dns.unshift(lanIp)
760 }
761 }
762
763 var qrm, qrs, qro;
764
765 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.'));
766 qrm.parent = parent;
767
768 qrs = qrm.section(form.NamedSection, 'config');
769
770 function handleConfigChange(ev, section_id, value) {
771 var code = this.map.findElement('.qr-code'),
772 conf = this.map.findElement('.client-config'),
773 endpoint = this.section.getUIElement(section_id, 'endpoint'),
774 ips = this.section.getUIElement(section_id, 'allowed_ips');
775 eips = this.section.getUIElement(section_id, 'addresses');
776 dns = this.section.getUIElement(section_id, 'dns_servers');
777
778 if (this.isValid(section_id)) {
779 conf.firstChild.data = configGenerator(endpoint.getValue(), ips.getValue(), eips.getValue(), dns.getValue());
780 code.style.opacity = '.5';
781
782 invokeQREncode(conf.firstChild.data, code);
783 }
784 };
785
786 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.'));
787 qro.datatype = 'or(ipaddr,hostname)';
788 hostnames.forEach(function(hostname) { qro.value(hostname) });
789 qro.onchange = handleConfigChange;
790
791 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.'));
792 qro.datatype = 'ipaddr';
793 qro.default = ips;
794 ips.forEach(function(ip) { qro.value(ip) });
795 qro.onchange = handleConfigChange;
796
797 qro = qrs.option(form.DynamicList, 'addresses', _('Addresses'), _('IP addresses for the peer to use inside the tunnel. Some clients require this setting.'));
798 qro.datatype = 'ipaddr';
799 qro.default = eips;
800 qro.default = dns;
801 eips.forEach(function(eip) { qro.value(eip) });
802 qro.onchange = handleConfigChange;
803
804 qro = qrs.option(form.DummyValue, 'output');
805 qro.renderWidget = function() {
806 var peer_config = configGenerator(hostnames[0], ips, eips, dns);
807
808 var node = E('div', {
809 'style': 'display:flex;flex-wrap:wrap;align-items:center;gap:.5em;width:100%'
810 }, [
811 E('div', {
812 'class': 'qr-code',
813 'style': 'width:320px;flex:0 1 320px;text-align:center'
814 }, [
815 E('em', { 'class': 'spinning' }, [ _('Generating QR code…') ])
816 ]),
817 E('pre', {
818 'class': 'client-config',
819 'style': 'flex:1;white-space:pre;overflow:auto',
820 'click': function(ev) {
821 var sel = window.getSelection(),
822 range = document.createRange();
823
824 range.selectNodeContents(ev.currentTarget);
825
826 sel.removeAllRanges();
827 sel.addRange(range);
828 }
829 }, [ peer_config ])
830 ]);
831
832 invokeQREncode(peer_config, node.firstChild);
833
834 return node;
835 };
836
837 return qrm.render().then(function(nodes) {
838 mapNode.classList.add('hidden');
839 mapNode.nextElementSibling.classList.add('hidden');
840
841 headNode.appendChild(E('span', [ ' » ', _('Generate configuration') ]));
842 mapNode.parentNode.appendChild(E([], [
843 nodes,
844 E('div', {
845 'class': 'right'
846 }, [
847 E('button', {
848 'class': 'btn',
849 'click': function() {
850 nodes.parentNode.removeChild(nodes.nextSibling);
851 nodes.parentNode.removeChild(nodes);
852 mapNode.classList.remove('hidden');
853 mapNode.nextSibling.classList.remove('hidden');
854 headNode.removeChild(headNode.lastChild);
855 }
856 }, [ _('Back to peer configuration') ])
857 ])
858 ]));
859
860 if (!s.formvalue(s.section, 'listen_port')) {
861 nodes.appendChild(E('div', { 'class': 'alert-message' }, [
862 E('p', [
863 _('No fixed interface listening port defined, peers might not be able to initiate connections to this WireGuard instance!')
864 ])
865 ]));
866 }
867 });
868 });
869 };
870
871 o.cfgvalue = function(section_id, value) {
872 var privkey = this.section.cfgvalue(section_id, 'private_key');
873
874 return E('button', {
875 'class': 'btn qr-code',
876 'style': 'display:inline-flex;align-items:center;gap:.5em',
877 'click': ui.createHandlerFn(this, 'handleGenerateQR', section_id),
878 'disabled': privkey ? null : ''
879 }, [
880 Object.assign(E(qrIcon), { style: 'width:22px;height:22px' }),
881 _('Generate configuration…')
882 ]);
883 };
884 },
885
886 deleteConfiguration: function() {
887 uci.sections('network', 'wireguard_%s'.format(this.sid), function(s) {
888 uci.remove('network', s['.name']);
889 });
890 }
891 });