fwknopd.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. 'use strict';
  2. 'require fs';
  3. 'require dom';
  4. 'require view';
  5. 'require form';
  6. 'require ui';
  7. 'require tools.widgets as widgets';
  8. var domparser = new DOMParser();
  9. var QRCODE_VARIABLES = ['KEY_BASE64', 'KEY', 'HMAC_KEY_BASE64', 'HMAC_KEY'];
  10. var INVALID_KEYS = ['__CHANGEME__', 'CHANGEME'];
  11. function setOptionValue(map, section_id, option, value) {
  12. var option = L.toArray(map.lookupOption(option, section_id))[0];
  13. var uiEl = option ? option.getUIElement(section_id) : null;
  14. if (uiEl)
  15. uiEl.setValue(value);
  16. }
  17. function lines(content) {
  18. return content.split(/\r?\n/);
  19. }
  20. function parseLine(rawLine) {
  21. if (rawLine[0] != '#' && rawLine[0] != ';') {
  22. var line = rawLine.split(/ ([^;]*)/, 2);
  23. if (line.length == 2) {
  24. var key = line[0].trim();
  25. var value = line[1].trim();
  26. if (key && value)
  27. return [key, value];
  28. }
  29. }
  30. return null;
  31. }
  32. function parseKeys(content) {
  33. var l = lines(content);
  34. var keys = {};
  35. for (var i = 0; i < l.length; i++) {
  36. var p = l[i].split(/:(.*)/, 2);
  37. if (p.length == 2)
  38. keys[p[0].trim()] = p[1].trim();
  39. }
  40. return keys;
  41. }
  42. var KeyTypeValue = form.ListValue.extend({
  43. __init__: function() {
  44. this.super('__init__', arguments);
  45. this.hidden = false;
  46. },
  47. cfgvalue: function(section_id) {
  48. for (var i = 0; i < this.keylist.length; i++) {
  49. var value = this.map.data.get(
  50. this.uciconfig || this.section.uciconfig || this.map.config,
  51. this.ucisection || section_id,
  52. this.keylist[i]
  53. );
  54. if (value)
  55. return this.keylist[i];
  56. }
  57. return this.keylist[0];
  58. },
  59. render: function(section_id, option_index, cfgvalue) {
  60. return this.super('render', arguments)
  61. .then(L.bind(function(el) {
  62. // Use direct style to hide, because class .hidden
  63. // is used by this.isActive(). We want full functionality,
  64. // but hidden field
  65. if (this.hidden)
  66. el.style.display = 'none';
  67. return el;
  68. }, this));
  69. },
  70. remove: function() {
  71. // Ignore
  72. },
  73. write: function() {
  74. // Ignore
  75. },
  76. });
  77. var YNValue = form.Flag.extend({
  78. __init__: function() {
  79. this.super('__init__', arguments);
  80. this.enabled = 'Y';
  81. this.disabled = 'N';
  82. this.default = 'N';
  83. },
  84. cfgvalue: function(section_id) {
  85. var value = this.super('cfgvalue', arguments);
  86. return value ? String(value).toUpperCase() : value;
  87. },
  88. parse: function(section_id) {
  89. var active = this.isActive(section_id),
  90. cval = this.cfgvalue(section_id),
  91. fval = active ? this.formvalue(section_id) : null;
  92. if (String(fval).toUpperCase() != cval) {
  93. if (fval == 'Y')
  94. return Promise.resolve(this.write(section_id, fval));
  95. else if (cval !== undefined)
  96. return Promise.resolve(this.remove(section_id));
  97. }
  98. },
  99. });
  100. var QrCodeValue = form.DummyValue.extend({
  101. __init__: function() {
  102. this.super('__init__', arguments);
  103. this.needsRefresh = {};
  104. this.components = [];
  105. QRCODE_VARIABLES.forEach(L.bind(function(option) {
  106. this.components.push(option);
  107. var dep = {};
  108. dep[option] = /.+/;
  109. this.depends(dep);
  110. }, this));
  111. },
  112. cfgQrCode: function(section_id) {
  113. var qr = [];
  114. for (var i = 0; i < this.components.length; i++) {
  115. var value = this.map.data.get(
  116. this.uciconfig || this.section.uciconfig || this.map.config,
  117. this.ucisection || section_id,
  118. this.components[i]
  119. );
  120. if (value)
  121. qr.push(this.components[i] + ':' + value);
  122. }
  123. return qr ? qr.join(' ') : null;
  124. },
  125. formQrCode: function(section_id) {
  126. var qr = [];
  127. for (var i = 0; i < this.components.length; i++) {
  128. var value = null;
  129. var uiEl = L.toArray(this.map.lookupOption(this.components[i], section_id))[0];
  130. if (uiEl) {
  131. if (uiEl.isActive(section_id))
  132. value = uiEl.formvalue(section_id);
  133. }
  134. if (value)
  135. qr.push(this.components[i] + ':' + value);
  136. }
  137. return qr ? qr.join(' ') : null;
  138. },
  139. onchange: function(ev, section_id) {
  140. if (this.needsRefresh[section_id] !== undefined)
  141. this.needsRefresh[section_id] = true;
  142. else {
  143. this.refresh(section_id);
  144. }
  145. },
  146. refresh: function(section_id) {
  147. var qrcode = this.formQrCode(section_id);
  148. var formvalue = this.formvalue(section_id);
  149. if (formvalue != qrcode) {
  150. this.getUIElement(section_id).setValue(qrcode);
  151. var uiEl = document.getElementById(this.cbid(section_id));
  152. if (uiEl) {
  153. var contentEl = uiEl.nextSibling;
  154. if (contentEl.childNodes.length == 1) {
  155. dom.append(contentEl, E('em', { 'class': 'spinning', }, [ _('Loading…') ]));
  156. }
  157. this.needsRefresh[section_id] = false;
  158. // Render QR code
  159. return this.renderSvg(qrcode)
  160. .then(L.bind(function(svgEl) {
  161. dom.content(contentEl, svgEl || E('div'));
  162. var needsAnotherRefresh = this.needsRefresh[section_id];
  163. delete this.needsRefresh[section_id];
  164. if (needsAnotherRefresh) {
  165. this.refresh(section_id);
  166. }
  167. }, this)).finally(L.bind(function() {
  168. if (this.needsRefresh[section_id] === undefined) {
  169. if (contentEl.childNodes.length == 2)
  170. contentEl.removeChild(contentEl.lastChild);
  171. delete this.needsRefresh[section_id];
  172. }
  173. }, this)).catch(L.error);
  174. }
  175. }
  176. // Nothing to render
  177. return Promise.resolve(null);
  178. },
  179. renderWidget: function(section_id) {
  180. var qrcode = this.cfgQrCode(section_id);
  181. return this.renderSvg(qrcode)
  182. .then(L.bind(function(svgEl) {
  183. var uiEl = new ui.Hiddenfield(qrcode, { id: this.cbid(section_id) });
  184. return E([
  185. uiEl.render(),
  186. E('div', {}, svgEl || E('div'))
  187. ]);
  188. }, this));
  189. },
  190. qrEncodeSvg: function(qrcode) {
  191. return fs.exec('/usr/bin/qrencode', ['--type', 'svg', '--inline', '-o', '-', qrcode])
  192. .then(function(response) {
  193. return response.stdout;
  194. });
  195. },
  196. renderSvg: function(qrcode) {
  197. if (qrcode)
  198. return this.qrEncodeSvg(qrcode)
  199. .then(function(rawsvg) {
  200. return domparser.parseFromString(rawsvg, 'image/svg+xml')
  201. .querySelector('svg');
  202. });
  203. else
  204. return Promise.resolve(null);
  205. },
  206. });
  207. var GenerateButton = form.Button.extend({
  208. __init__: function() {
  209. this.super('__init__', arguments);
  210. this.onclick = L.bind(this.generateKeys, this);
  211. this.keytypes = {};
  212. },
  213. keytype: function(key, regex) {
  214. this.keytypes[key] = regex;
  215. },
  216. qrcode: function(option) {
  217. this.qrcode = option;
  218. },
  219. generateKeys: function(ev, section_id) {
  220. return fs.exec('/usr/sbin/fwknopd', ['--key-gen'])
  221. .then(function(response) { return parseKeys(response.stdout); })
  222. .then(L.bind(this.applyKeys, this, section_id))
  223. .catch(L.error);
  224. },
  225. applyKeys: function(section_id, keys) {
  226. for (var key in keys) {
  227. setOptionValue(this.map, section_id, key, keys[key]);
  228. for (var type in this.keytypes) {
  229. if (this.keytypes[type].test(key))
  230. setOptionValue(this.map, section_id, type, key);
  231. }
  232. }
  233. // Force update of dependencies (element visibility)
  234. this.map.checkDepends();
  235. // Refresh QR code
  236. var option = L.toArray(this.map.lookupOption(this.qrcode, section_id))[0];
  237. if (option)
  238. return option.refresh(section_id);
  239. else
  240. return Promise.resolve(null);
  241. },
  242. });
  243. var ParseButton = form.Button.extend({
  244. __init__: function() {
  245. this.super('__init__', arguments);
  246. this.onclick = L.bind(this.parseAccessConf, this);
  247. },
  248. parseAccessConf: function() {
  249. this.stanzas = [];
  250. var ctx = {
  251. processLine: L.bind(this.processAccessLine, this),
  252. remainingLines: [],
  253. stanzas: {
  254. last: {},
  255. all: []
  256. }
  257. };
  258. return fs.read('/etc/fwknop/access.conf')
  259. .then(L.bind(this.parseFile, this, ctx))
  260. .then(L.bind(function() {
  261. if (ctx.stanzas.all.length > 0)
  262. return this.renderStanzas(ctx.stanzas.all)
  263. .then(function(topEl) {
  264. var dlg = ui.showModal(_('Firewall Knock Operator Daemon'), [
  265. topEl,
  266. E('button', {
  267. 'class': 'cbi-button cbi-button-neutral',
  268. 'click': ui.hideModal
  269. }, _('Close'))
  270. ], 'cbi-modal');
  271. dlg.querySelector('button').focus();
  272. dlg.parentNode.scrollTop = 0;
  273. });
  274. else {
  275. var dlg = ui.showModal(_('Firewall Knock Operator Daemon'), [
  276. E('p', _("No stanza found.")),
  277. E('button', {
  278. 'class': 'cbi-button cbi-button-neutral',
  279. 'click': ui.hideModal
  280. }, _('Close'))
  281. ]);
  282. dlg.querySelector('button').focus();
  283. }
  284. }, this))
  285. .catch(function(err) {
  286. L.error(err);
  287. });
  288. },
  289. parseFile: function(ctx, content) {
  290. ctx.remainingLines.unshift.apply(ctx.remainingLines, lines(content));
  291. return this.parseLines(ctx);
  292. },
  293. parseFolder: function(ctx, folder, entries) {
  294. // Parse and process files in order
  295. var parseJobs = [];
  296. var parsedLines = [];
  297. entries.sort(function(el1, el2) {
  298. return (el1.name > el2.name) ? 1
  299. : (el1.name < el2.name) ? -1
  300. : 0;
  301. });
  302. entries.forEach(L.bind(function(entry) {
  303. var ctxLines = [];
  304. parsedLines.unshift(ctxLines);
  305. parseJobs.push(fs.read(folder + '/' + entry.name)
  306. .then(function(content) {
  307. ctxLines.push.apply(ctxLines, lines(content));
  308. }));
  309. }, this));
  310. return Promise.all(parseJobs)
  311. .then(L.bind(function(ctx) {
  312. parsedLines.forEach(function(lines) {
  313. ctx.remainingLines.unshift.apply(ctx.remainingLines, lines);
  314. });
  315. }, this, ctx))
  316. .then(L.bind(this.parseLines, this, ctx));
  317. },
  318. parseLines: function(ctx) {
  319. while (ctx.remainingLines.length > 0) {
  320. var line = parseLine(ctx.remainingLines.shift());
  321. if (line) {
  322. var result = ctx.processLine.call(this, ctx, line[0], line[1]);
  323. if (result)
  324. return result;
  325. }
  326. }
  327. },
  328. processAccessLine: function(ctx, key, value) {
  329. if (key.endsWith(':')) {
  330. key = key.slice(0, -1);
  331. }
  332. if (key == "%include") {
  333. return fs.read(value)
  334. .then(L.bind(this.parseFile, this, ctx));
  335. } else if (key == "%include_folder") {
  336. return fs.list(value)
  337. .then(L.bind(this.parseFolder, this, ctx, value));
  338. } else if (key == "%include_keys") {
  339. var keysCtx = {
  340. processLine: L.bind(this.processKeysLine, this),
  341. remainingLines: [],
  342. stanzas: ctx.stanzas
  343. };
  344. return fs.read(value)
  345. .then(L.bind(this.parseFile, this, keysCtx))
  346. .then(L.bind(this.parseLines, this, ctx));
  347. } else {
  348. if (key == 'SOURCE') {
  349. ctx.stanzas.last = {};
  350. ctx.stanzas.all.push(ctx.stanzas.last);
  351. }
  352. ctx.stanzas.last[key] = value;
  353. }
  354. },
  355. processKeysLine: function(ctx, key, value) {
  356. // Simplification - accept only KEY arguments
  357. if (ctx.stanzas.last && key.match(/KEY/))
  358. ctx.stanzas.last[key] = value;
  359. },
  360. renderStanzas: function(stanzas) {
  361. var config = {};
  362. config.access = stanzas;
  363. var m, s, o;
  364. m = new form.JSONMap(config, null, _('Custom configuration read from /etc/fwknop/access.conf.'));
  365. m.readonly = true;
  366. // set the access.conf settings
  367. s = m.section(form.TypedSection, 'access', _('access.conf stanzas'));
  368. s.anonymous = true;
  369. o = s.option(QrCodeValue, 'qr', _('QR code'), ('QR code to configure fwknopd Android application.'));
  370. o = s.option(form.Value, 'SOURCE', 'SOURCE');
  371. o = s.option(form.Value, 'DESTINATION', 'DESTINATION');
  372. o = s.option(form.Value, 'KEY', 'KEY');
  373. o.depends('keytype', 'KEY');
  374. o.validate = function(section_id, value) {
  375. return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The symmetric key has to be specified.');
  376. }
  377. o = s.option(form.Value, 'KEY_BASE64', 'KEY_BASE64');
  378. o.depends('keytype', 'KEY_BASE64');
  379. o.validate = function(section_id, value) {
  380. return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The symmetric key has to be specified.');
  381. }
  382. o = s.option(KeyTypeValue, 'keytype');
  383. o.value('KEY', _('Normal key'));
  384. o.value('KEY_BASE64', _('Base64 key'));
  385. o.hidden = true;
  386. o = s.option(form.Value, 'HMAC_KEY', 'HMAC_KEY');
  387. o.depends('hkeytype', 'HMAC_KEY');
  388. o.validate = function(section_id, value) {
  389. return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The HMAC authentication key has to be specified.');
  390. }
  391. o = s.option(form.Value, 'HMAC_KEY_BASE64', 'HMAC_KEY_BASE64');
  392. o.depends('hkeytype', 'HMAC_KEY_BASE64');
  393. o.validate = function(section_id, value) {
  394. return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The HMAC authentication key has to be specified.');
  395. }
  396. o = s.option(KeyTypeValue, 'hkeytype');
  397. o.value('HMAC_KEY', _('Normal key'));
  398. o.value('HMAC_KEY_BASE64', _('Base64 key'));
  399. o.hidden = true;
  400. return m.load()
  401. .then(L.bind(m.render, m));
  402. }
  403. });
  404. return view.extend({
  405. load: function() {
  406. return Promise.all([
  407. L.resolveDefault(fs.stat('/etc/fwknop/access.conf'))
  408. ]);
  409. },
  410. render: function(results) {
  411. var has_access_conf = results[0];
  412. var m, s, o;
  413. m = new form.Map('fwknopd', _('Firewall Knock Operator Daemon'));
  414. s = m.section(form.TypedSection, 'global', _('Enable Uci/Luci control'));
  415. s.anonymous = true;
  416. s.option(form.Flag, 'uci_enabled', _('Enable config overwrite'), _('When unchecked, the config files in /etc/fwknopd will be used as is, ignoring any settings here.'));
  417. if ( has_access_conf ) {
  418. o = s.option(ParseButton, 'parse', _('Custom configuration'), _('Parses the /etc/fwknop/access.conf file (and \
  419. included files/folders/keys) and generates QR codes for all found \
  420. stanzas. Handles only files in /etc/fwknop folder due to access rights \
  421. restrictions.'));
  422. o.inputtitle = _("Show access.conf QR codes");
  423. }
  424. s = m.section(form.TypedSection, 'network', _('Network configuration'));
  425. s.anonymous = true;
  426. o = s.option(widgets.NetworkSelect, 'network', _('Network'), _('The network on which the daemon listens. The daemon \
  427. is automatically started when the network is up-and-running. This option \
  428. has precedence over “PCAP_INTF” option.'));
  429. o.unpecified = true;
  430. o.nocreate = true;
  431. o.rmempty = true;
  432. // set the access.conf settings
  433. s = m.section(form.TypedSection, 'access', _('access.conf stanzas'));
  434. s.anonymous = true;
  435. s.addremove = true;
  436. var qrCode = s.option(QrCodeValue, 'qr', _('QR code'), ('QR code to configure fwknopd Android application.'));
  437. o = s.option(form.Value, 'SOURCE', 'SOURCE', _('The source address from which the SPA packet will be accepted. The string “ANY” is \
  438. also accepted if a valid SPA packet should be honored from any source IP. \
  439. Networks should be specified in CIDR notation (e.g. “192.168.10.0/24”), \
  440. and individual IP addresses can be specified as well. Multiple entries \
  441. are comma-separated.'));
  442. o.validate = function(section_id, value) {
  443. return String(value).length > 0 ? true : _('The source address has to be specified.');
  444. }
  445. s.option(form.Value, 'DESTINATION', 'DESTINATION', _('The destination address for which the SPA packet will be accepted. The \
  446. string “ANY” is also accepted if a valid SPA packet should be honored to any \
  447. destination IP. Networks should be specified in CIDR notation \
  448. (e.g. “192.168.10.0/24”), and individual IP addresses can be specified as well. \
  449. Multiple entries are comma-separated.'));
  450. o = s.option(GenerateButton, 'keys', _('Generate keys'), _('Generates the symmetric key used for decrypting an incoming \
  451. SPA packet, that is encrypted by the fwknop client with Rijndael block cipher, \
  452. and HMAC authentication key used to verify the authenticity of the incoming SPA \
  453. packet before the packet is decrypted.'));
  454. o.inputtitle = _("Generate Keys");
  455. o.keytype('keytype', /^KEY/);
  456. o.keytype('hkeytype', /^HMAC_KEY/);
  457. o.qrcode('qr');
  458. o = s.option(form.Value, 'KEY', 'KEY', _('Define the symmetric key used for decrypting an incoming SPA \
  459. packet that is encrypted by the fwknop client with Rijndael.'));
  460. o.depends('keytype', 'KEY');
  461. o.onchange = L.bind(qrCode.onchange, qrCode);
  462. o.validate = function(section_id, value) {
  463. return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The symmetric key has to be specified.');
  464. }
  465. o = s.option(form.Value, 'KEY_BASE64', 'KEY_BASE64', _('Define the symmetric key (in Base64 encoding) used for \
  466. decrypting an incoming SPA packet that is encrypted by the fwknop client \
  467. with Rijndael.'));
  468. o.depends('keytype', 'KEY_BASE64');
  469. o.onchange = L.bind(qrCode.onchange, qrCode);
  470. o.validate = function(section_id, value) {
  471. return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The symmetric key has to be specified.');
  472. }
  473. o = s.option(KeyTypeValue, 'keytype', _('Key type'));
  474. o.value('KEY', _('Normal key'));
  475. o.value('KEY_BASE64', _('Base64 key'));
  476. o.onchange = L.bind(qrCode.onchange, qrCode);
  477. o = s.option(form.Value, 'HMAC_KEY', 'HMAC_KEY', _('Define the HMAC authentication key used for verifying \
  478. the authenticity of the SPA packet before the packet is decrypted.'));
  479. o.depends('hkeytype', 'HMAC_KEY');
  480. o.onchange = L.bind(qrCode.onchange, qrCode);
  481. o.validate = function(section_id, value) {
  482. return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The HMAC authentication key has to be specified.');
  483. }
  484. o = s.option(form.Value, 'HMAC_KEY_BASE64', 'HMAC_KEY_BASE64', _('Define the HMAC authentication key \
  485. (in Base64 encoding) used for verifying the authenticity of the SPA \
  486. packet before the packet is decrypted.'));
  487. o.depends('hkeytype', 'HMAC_KEY_BASE64');
  488. o.onchange = L.bind(qrCode.onchange, qrCode);
  489. o.validate = function(section_id, value) {
  490. return (String(value).length > 0 && !INVALID_KEYS.includes(value)) ? true : _('The HMAC authentication key has to be specified.');
  491. }
  492. o = s.option(KeyTypeValue, 'hkeytype', _('HMAC key type'));
  493. o.value('HMAC_KEY', _('Normal key'));
  494. o.value('HMAC_KEY_BASE64', _('Base64 key'));
  495. o.onchange = L.bind(qrCode.onchange, qrCode);
  496. o = s.option(form.Value, 'OPEN_PORTS', 'OPEN_PORTS', _('Define a set of ports and protocols (tcp or udp) that will be opened if a valid knock sequence is seen. \
  497. If this entry is not set, fwknopd will attempt to honor any proto/port request specified in the SPA data \
  498. (unless of it matches any “RESTRICT_PORTS” entries). Multiple entries are comma-separated.'));
  499. o.placeholder = "protocol/port,...";
  500. o = s.option(form.Value, 'RESTRICT_PORTS', 'RESTRICT_PORTS', _('Define a set of ports and protocols (tcp or udp) that are explicitly not allowed \
  501. regardless of the validity of the incoming SPA packet. Multiple entries are comma-separated.'));
  502. o.placeholder = "protocol/port,...";
  503. o = s.option(form.Value, 'FW_ACCESS_TIMEOUT', 'FW_ACCESS_TIMEOUT', _('Define the length of time access will be granted by fwknopd through the firewall after a \
  504. valid knock sequence from a source IP address. If “FW_ACCESS_TIMEOUT” is not set then the default \
  505. timeout of 30 seconds will automatically be set.'));
  506. o.placeholder = "30";
  507. s.option(YNValue, 'REQUIRE_SOURCE_ADDRESS', 'REQUIRE_SOURCE_ADDRESS', _('Force all SPA packets to contain a real IP address within the encrypted data. \
  508. This makes it impossible to use the -s command line argument on the fwknop client command line, so either -R \
  509. has to be used to automatically resolve the external address (if the client behind a NAT) or the client must \
  510. know the external IP and set it via the -a argument.'));
  511. s.option(YNValue, 'ENABLE_CMD_EXEC', 'ENABLE_CMD_EXEC', _('This instructs fwknopd to accept complete commands that are contained within an authorization packet. \
  512. Any such command will be executed on the fwknopd server as the user specified by the “CMD_EXEC_USER” or as the user \
  513. that started fwknopd if that is not set.'));
  514. s = m.section(form.TypedSection, 'config', _('fwknopd.conf config options'));
  515. s.anonymous=true;
  516. s.option(form.Value, 'MAX_SPA_PACKET_AGE', 'MAX_SPA_PACKET_AGE', _('Maximum age in seconds that an SPA packet will be accepted. Defaults to 120 seconds.'));
  517. s.option(form.Value, 'PCAP_INTF', 'PCAP_INTF', _('Specify the ethernet interface on which fwknopd will sniff packets.'));
  518. s.option(YNValue, 'ENABLE_IPT_FORWARDING', 'ENABLE_IPT_FORWARDING', _('Allow SPA clients to request access to services through an iptables firewall instead of just to it.'));
  519. s.option(YNValue, 'ENABLE_NAT_DNS', 'ENABLE_NAT_DNS', _('Allow SPA clients to request forwarding destination by DNS name.'));
  520. return m.render();
  521. }
  522. });