wireguard.js 32 KB

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