diff --git a/applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/strongswan.js b/applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/strongswan.js index e1ce0118f4ad..bc4e0aed8b52 100644 --- a/applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/strongswan.js +++ b/applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/strongswan.js @@ -3,16 +3,28 @@ 'require view'; 'require tools.widgets as widgets'; +function addLogLevel(o) { + o.value('', _('Use daemon default')); + o.value('-1', _('Absolutely silent')); + o.value('0', _('Very basic auditing logs')); + o.value('1', _('Generic control flow with errors (default)')); + o.value('2', _('More detailed debugging control flow')); + o.value('3', _('Including RAW data dumps in hex')); + o.value('4', _('Also include sensitive material in dumps, e.g. keys')); + o.default = ''; +} + return view.extend({ render: function (result) { let m, s, o; - m = new form.Map('ipsec', _('Service configuration'), + m = new form.Map('swanctl', _('Service configuration'), _('On this page, you can configure the IPsec service.')); m.tabbed = true; - // strongSwan General Settings - s = m.section(form.NamedSection, 'globals', 'ipsec', _('General Settings')); + // general settings + s = m.section(form.NamedSection, 'globals', 'globals', _('General Settings'), + _('Configure global service parameters.')); o = s.option(widgets.NetworkSelect, 'interface', _('Listening Interfaces'), _('Interfaces that accept VPN traffic.') + '
' + @@ -21,10 +33,74 @@ return view.extend({ o.nocreate = true; o.optional = true; - o = s.option(form.Value, 'debug', _('Debug Level'), - _('Trace level: 0 is least verbose, 4 is most')); + // syslog plugin settings + s = m.section(form.NamedSection, 'syslog', 'syslog', _('Syslog Settings'), + _('Configure how strongswan logs events, errors, and debug information.')); + + o = s.option(form.ListValue, 'app', 'app', _('Applications other than daemons')); + addLogLevel(o); + + o = s.option(form.ListValue, 'asn', 'asn', _('Low-level encoding/decoding (ASN.1, X.509 etc.)')); + addLogLevel(o); + + o = s.option(form.ListValue, 'cfg', 'cfg', _('Configuration management and plugins')); + addLogLevel(o); + + o = s.option(form.ListValue, 'chd', 'chd', _('CHILD_SA/IPsec_SA')); + addLogLevel(o); + + o = s.option(form.ListValue, 'dmn', 'dmn', _('Main daemon setup/cleanup/signal handling')); + addLogLevel(o); + + o = s.option(form.ListValue, 'enc', 'enc', _('Packet encoding/decoding encryption/decryption operations')); + addLogLevel(o); + + o = s.option(form.ListValue, 'esp', 'esp', '%s (%s)'.format(_('Library messages'), 'libipsec')); + addLogLevel(o); + + o = s.option(form.ListValue, 'ike', 'ike', _('IKE_SA / ISAKMP SA')); + addLogLevel(o); + + o = s.option(form.ListValue, 'imc', 'imc', _('Integrity Measurement Collector')); + addLogLevel(o); + + o = s.option(form.ListValue, 'imv', 'imv', _('Integrity Measurement Verifier')); + addLogLevel(o); + + o = s.option(form.ListValue, 'job', 'job', _('Jobs queuing/processing and thread pool management')); + addLogLevel(o); + + o = s.option(form.ListValue, 'wch', 'wch', _('File descriptor watcher')); + addLogLevel(o); + + o = s.option(form.ListValue, 'knl', 'knl', _('IPsec/Networking kernel interface')); + addLogLevel(o); + + o = s.option(form.ListValue, 'lib', 'lib', '%s (%s)'.format(_('Library messages'), 'libstrongswan')); + addLogLevel(o); + + o = s.option(form.ListValue, 'mgr', 'mgr', _('IKE_SA manager, handling synchronization for IKE_SA access')); + addLogLevel(o); + + o = s.option(form.ListValue, 'net', 'net', _('IKE network communication')); + addLogLevel(o); + + o = s.option(form.ListValue, 'pts', 'pts', _('Platform Trust Service')); + addLogLevel(o); + + o = s.option(form.ListValue, 'tls', 'tls', '%s (%s)'.format(_('Library messages'), 'libtls')); + addLogLevel(o); + + o = s.option(form.ListValue, 'tnc', 'tnc', _('Trusted Network Connect')); + addLogLevel(o); + + // kernel-netlink plugin settings + s = m.section(form.NamedSection, 'netlink', 'netlink', _('Netlink Settings'), + _('Configure the kernel-netlink plugin options.')); + + o = s.option(form.Flag, 'install_routes_xfrmi', _('Install routes via XFRM interfaces'), + _('When this is enabled, the routes for all XFRM interfaces are set automatically.')); o.default = '0'; - o.datatype = 'range(0,4)'; return m.render(); } diff --git a/applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/swanctl.js b/applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/swanctl.js index 07288aef5f15..09c857d9c1b8 100644 --- a/applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/swanctl.js +++ b/applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/swanctl.js @@ -4,6 +4,7 @@ 'require rpc'; 'require uci'; 'require ui'; +'require fs'; const callListAlgorithms = rpc.declare({ object: 'luci.swanctl', @@ -36,37 +37,108 @@ function sectionNameCheck(extra_class) { nameEl = el.querySelector('.cbi-section-create-name'); ui.addValidator(nameEl, 'uciname', true, function(v) { let sections = [ - ...uci.sections('ipsec', 'remote'), - ...uci.sections('ipsec', 'tunnel'), - ...uci.sections('ipsec', 'crypto_proposal'), + ...uci.sections('swanctl', 'remote'), + ...uci.sections('swanctl', 'child'), + ...uci.sections('swanctl', 'shunt'), + ...uci.sections('swanctl', 'crypto_proposal'), ]; if (sections.find(function(s) { return s['.name'] == v; })) { - return _('Remotes, Encryption Proposals and Tunnels may not share the same names.') + ' ' + - _('Use combinations like tunnel1_phase1 that do not exceed 15 characters.'); + return _('Remotes, Encryption Proposals, Children and Shunts may not share the same names.') + ' ' + + _('Use combinations like child1_phase1.'); } - if (v.length > 15) return _('Name length shall not exceed 15 characters'); return true; }, 'blur', 'keyup'); return el; }; +let migrationOverlay = null; + +function renderMigrationContent(errorMessage) { + const dialog = migrationOverlay.firstElementChild; + const migrateButton = E('button', { + 'class': 'btn cbi-button cbi-button-apply', + 'click': handleMigrate + }, [_('Migrate')]); + + const content = E([], [ + E('h4', _('Migrate IPsec configuration')), + E('p', _('A legacy IPsec configuration was found.') + ' ' + + _('It must be migrated from the uci config \'/etc/config/ipsec\' ' + + 'to \'/etc/config/swanctl\' before it can be managed here.')), + E('p', _('After a successful migration, the file \'/etc/config/ipsec\' is moved to \'/etc/config/ipsec.bak\'.') + ' ' + + _('The migration may require manual intervention.') + ' ' + + _('Therefore, the VPN is not restarted.')), + E('p', _('Warning: The migration will drop all IPsec VPN connections.')), + E('p', _('This will execute the command \'/etc/init.d/swanctl migrate\'.')) + ]); + + if (errorMessage) + content.appendChild(E('p', errorMessage)); + + content.appendChild(E('div', { 'class': 'right' }, [migrateButton])); + + dialog.replaceChildren(content); +} + +function showMigrationOverlay(viewNode) { + if (migrationOverlay) + return; + + const dialog = E('div', { 'class': 'modal', 'style': 'margin:0;' }); + migrationOverlay = E('div', { + 'class': 'migration-overlay', + 'style': 'position:absolute;top:0;right:0;bottom:0;left:0;z-index:900;display:flex;align-items:center;justify-content:center;padding:1em;pointer-events:all;' + }, [dialog]); + + viewNode.style.position = 'relative'; + viewNode.appendChild(migrationOverlay); + + const maincontent = document.getElementById('maincontent'); + if (maincontent) + maincontent.style.pointerEvents = 'none'; + + renderMigrationContent(null); +} + +function handleMigrate() { + const dialog = migrationOverlay.firstElementChild; + + dialog.replaceChildren(E([], [ + E('h4', _('Migrate IPsec configuration')), + E('p', _('Migrating IPsec configuration…')) + ])); + + fs.exec('/etc/init.d/swanctl', ['migrate']).then(function (result) { + if (result.code != 0) + renderMigrationContent(_('Migration failed: %s').format(result.stderr || result.stdout || _('unknown error'))); + else + window.location.reload(); + }).catch(function (e) { + renderMigrationContent(_('Migration failed: %s').format(e.message)); + }); +} + return view.extend({ load: async function () { await uci.load('network'); - return await callListAlgorithms(); + const algorithms = await callListAlgorithms(); + const legacyConfig = await L.resolveDefault(fs.stat('/etc/config/ipsec'), null); + + return { algorithms, legacyConfig }; }, render: function (result) { let m, s, o; - const algorithms = result.data ?? {}; - const error = result.error; + const legacyConfig = !!result.legacyConfig; + const algorithms = result.algorithms.data ?? {}; + const error = result.algorithms.error; if (error) ui.addNotification(null, E('p', _('Some options are unavailable because swanctl failed to load: %s').format(error)), 'warning'); - m = new form.Map('ipsec', _('Connection configurations'), + m = new form.Map('swanctl', _('Connection configurations'), _('On this page, you can configure the IPsec connections.')); m.tabbed = true; @@ -109,7 +181,7 @@ return view.extend({ this.keylist = []; this.vallist = []; - var sections = uci.sections('ipsec', 'crypto_proposal').filter(function (section) { + var sections = uci.sections('swanctl', 'crypto_proposal').filter(function (section) { return section.is_esp != '1'; }); if (sections.length == 0) { @@ -124,18 +196,18 @@ return view.extend({ }; o.rmempty = false; - o = s.taboption('general', form.MultiValue, 'tunnel', _('Tunnel'), - _('The Tunnel containing the ESP (phase 2) section')); + o = s.taboption('general', form.MultiValue, 'child', _('Children'), + _('The Children containing the ESP (phase 2) section')); o.load = function (section_id) { this.keylist = []; this.vallist = []; - var sections = uci.sections('ipsec', 'tunnel'); + var sections = uci.sections('swanctl', 'child'); if (sections.length == 0) { - this.value('', _('Please create a Tunnel first')); + this.value('', _('Please create a Children first')); } else { sections.forEach(L.bind(function (section) { - this.value(section['.name']); + this.value(section['.name'], '%s (%s)'.format(section['.name'], section['mode'])); }, this)); } @@ -231,7 +303,7 @@ return view.extend({ o.placeholder = '3'; o.modalonly = true; - o = s.taboption('advanced', form.Value, 'dpddelay', _('DPD Delay'), + o = s.taboption('advanced', form.Value, 'dpd_delay', _('DPD Delay'), _('Interval to check liveness of a peer')); o.validate = validateTimeFormat; o.placeholder = '30s'; @@ -243,12 +315,12 @@ return view.extend({ o.placeholder = '0s'; o.modalonly = true; - o = s.taboption('advanced', form.Value, 'rekeytime', _('Rekey Time'), + o = s.taboption('advanced', form.Value, 'rekey_time', _('Rekey Time'), _('IKEv2 interval to refresh keying material; also used to compute lifetime')); o.validate = validateTimeFormat; o.modalonly = true; - o = s.taboption('advanced', form.Value, 'overtime', _('Overtime'), + o = s.taboption('advanced', form.Value, 'over_time', _('Overtime'), _('Limit on time to complete rekeying/reauthentication')); o.validate = validateTimeFormat; o.modalonly = true; @@ -269,9 +341,9 @@ return view.extend({ o.default = 'ikev2'; o.modalonly = true; - // Tunnel Configuration - s = m.section(form.GridSection, 'tunnel', _('Tunnel Configuration'), - _('Define Connection Children to be used as Tunnels in Remote Configurations.')); + // Children Configuration + s = m.section(form.GridSection, 'child', _('Children Configuration'), + _('Define Connection Children to be used in Remote Configurations.')); s.addremove = true; s.nodescriptions = true; s.renderSectionAdd = sectionNameCheck; @@ -279,6 +351,12 @@ return view.extend({ o = s.tab('general', _('General')); o = s.tab('advanced', _('Advanced')); + o = s.taboption('general', form.ListValue, 'mode', _('Child mode')); + o.rmempty = false; + o.value('tunnel', _('Tunnel')); + o.value('transport', _('Transport')); + o.default = 'tunnel'; + o = s.taboption('general', form.DynamicList, 'local_subnet', _('Local Subnet'), _('Local network(s)')); o.datatype = 'cidr'; @@ -311,20 +389,24 @@ return view.extend({ o.optional = true; o.modalonly = true; - o = s.taboption('general', form.ListValue, 'startaction', _('Start Action'), + o = s.taboption('general', form.ListValue, 'start_action', _('Start Action'), _('Action on initial configuration load')); - o.value('none'); + o.value('', '%s (%s)'.format('none', _('default'))); o.value('trap'); o.value('start'); - o.default = 'trap'; + o.optional = true; + o.default = ''; + o.rmempty = true; o.modalonly = true; - o = s.taboption('general', form.ListValue, 'closeaction', _('Close Action'), + o = s.taboption('general', form.ListValue, 'close_action', _('Close Action'), _('Action when CHILD_SA is closed')); - o.value('none'); + o.value('', '%s (%s)'.format('none', _('default'))); o.value('trap'); o.value('start'); o.optional = true; + o.default = ''; + o.rmempty = true; o.modalonly = true; o = s.taboption('general', form.MultiValue, 'crypto_proposal', @@ -334,7 +416,7 @@ return view.extend({ this.keylist = []; this.vallist = []; - var sections = uci.sections('ipsec', 'crypto_proposal').filter(function (section) { + var sections = uci.sections('swanctl', 'crypto_proposal').filter(function (section) { return section.is_esp == '1'; }); if (sections.length == 0) { @@ -354,16 +436,17 @@ return view.extend({ o.datatype = 'file'; o.modalonly = true; - o = s.taboption('advanced', form.ListValue, 'dpdaction', _('DPD Action'), + o = s.taboption('advanced', form.ListValue, 'dpd_action', _('DPD Action'), _('Action when DPD timeout occurs')); - o.value('none'); - o.value('clear'); + o.value('', '%s (%s)'.format('clear', _('default'))); o.value('trap'); o.value('start'); + o.default = ''; + o.rmempty = true; o.optional = true; o.modalonly = true; - o = s.taboption('advanced', form.Value, 'rekeytime', _('Rekey Time'), + o = s.taboption('advanced', form.Value, 'rekey_time', _('Rekey Time'), _('Interval before a CHILD_SA is rekeyed.') + ' ' + _('Also used to derive lifetime (110% of this value).') + '
' + _('If not configured, the default value is "1h".') @@ -373,7 +456,7 @@ return view.extend({ o.rmempty = true; o.modalonly = true; - o = s.taboption('advanced', form.Value, 'lifetime', _('Life Time'), + o = s.taboption('advanced', form.Value, 'life_time', _('Life Time'), _('Maximum time before the CHILD_SA gets closed, as a hard limit.') ); o.validate = validateTimeFormat; @@ -404,7 +487,7 @@ return view.extend({ o.datatype = 'uinteger'; o.modalonly = true; - o = s.taboption('advanced', form.Value, 'rekeybytes', _('Rekey Bytes'), + o = s.taboption('advanced', form.Value, 'rekey_bytes', _('Rekey Bytes'), _('Number of bytes processed before initiating CHILD_SA rekeying.') + ' ' + _('Also used to derive lifebytes if set (110% of this value).') + ' ' + _('Use "0" to disable byte based rekeying.') @@ -414,7 +497,7 @@ return view.extend({ o.rmempty = true; o.modalonly = true; - o = s.taboption('advanced', form.Value, 'lifebytes', _('Life Bytes'), + o = s.taboption('advanced', form.Value, 'life_bytes', _('Life Bytes'), _('Maximum number of bytes processed before the CHILD_SA gets closed.') + ' ' + _('Use "0" to disable (default).') ); @@ -423,7 +506,7 @@ return view.extend({ o.rmempty = true; o.modalonly = true; - o = s.taboption('advanced', form.Value, 'rekeypackets', _('Rekey Packets'), + o = s.taboption('advanced', form.Value, 'rekey_packets', _('Rekey Packets'), _('Number of packets processed before initiating CHILD_SA rekeying.') + ' ' + _('Also used to derive lifepackets if set (110% of this value).') + ' ' + _('Use "0" to disable packet based rekeying (default).') @@ -433,7 +516,7 @@ return view.extend({ o.rmempty = true; o.modalonly = true; - o = s.taboption('advanced', form.Value, 'lifepackets', _('Life Packets'), + o = s.taboption('advanced', form.Value, 'life_packets', _('Life Packets'), _('Maximum number of packets processed before the CHILD_SA gets closed.') + ' ' + _('Use "0" to disable (default).') ); @@ -442,6 +525,40 @@ return view.extend({ o.rmempty = true; o.modalonly = true; + // Shunt Configuration + s = m.section(form.GridSection, 'shunt', _('Shunt Configuration'), + _('Define Shunt pass/drop policies independent of remote connections.')); + s.addremove = true; + s.nodescriptions = true; + s.renderSectionAdd = sectionNameCheck; + + o = s.option(form.Flag, 'enabled', _('Enabled'), + _('Configuration is enabled or not')); + o.rmempty = false; + + o = s.option(form.ListValue, 'mode', _('Mode'), + _('Shunt policy mode')); + o.value('pass', _('Pass')); + o.value('drop', _('Drop')); + o.rmempty = false; + + o = s.option(form.DynamicList, 'local_ts', _('Local Traffic Selectors'), + _('Local traffic selectors for the shunt policy')); + o.datatype = 'or(cidr,ipaddr)'; + + o = s.option(form.DynamicList, 'remote_ts', _('Remote Traffic Selectors'), + _('Remote traffic selectors for the shunt policy')); + o.datatype = 'or(cidr,ipaddr)'; + + o = s.option(form.Value, 'priority', _('Priority'), + _('Priority of the shunt policy (lower number means higher priority)')); + o.datatype = 'uinteger'; + o.modalonly = true; + + o = s.option(form.Value, 'interface', _('Interface'), + _('Network interface to bind the shunt policy to')); + o.modalonly = true; + // Crypto Proposals s = m.section(form.GridSection, 'crypto_proposal', _('Encryption Proposals'), @@ -453,19 +570,32 @@ return view.extend({ o = s.option(form.Flag, 'is_esp', _('ESP Proposal'), _('Whether this is an ESP (phase 2) proposal or not')); + o = s.option(form.Flag, 'use_custom_proposal', _('Use custom proposal'), + _('When enabled, you can specify your own proposal string.')); + o.default = '0'; + o.rmempty = true; + + o = s.option(form.Value, 'custom_proposal', _('Custom proposal'), + _('Using this option only if you know exactly what you are doing.') + '
' + + _('Manually defining a proposal can break compatibility with peers or cause connection failures.') + '
' + + _('Using this setting may prevent future updates or migrations.') + '
' + + _('You are responsible for maintaining compatibility!')); + o.depends('use_custom_proposal', '1'); + o.rmempty = false; + o = s.option(form.ListValue, 'encryption_algorithm', _('Encryption Algorithm'), _('Algorithms marked with * are considered insecure')); o.default = 'aes256gcm128'; + o.depends('use_custom_proposal', '0'); addAlgorithms(o, algorithms.encryption); addAlgorithms(o, algorithms.aead); - const encryptionAlgorithmNames = algorithms.encryption?.map(algorithm => algorithm.name); o = s.option(form.ListValue, 'hash_algorithm', _('Hash Algorithm'), _('Algorithms marked with * are considered insecure')); encryptionAlgorithmNames?.forEach(function (algorithmName) { - o.depends('encryption_algorithm', algorithmName); + o.depends({'encryption_algorithm': algorithmName, 'use_custom_proposal': '0'}); }); o.default = 'sha512'; o.rmempty = false; @@ -474,6 +604,7 @@ return view.extend({ o = s.option(form.ListValue, 'dh_group', _('Diffie-Hellman Group'), _('Algorithms marked with * are considered insecure')); o.default = 'modp3072'; + o.depends('use_custom_proposal', '0'); addAlgorithms(o, algorithms.ke); o = s.option(form.ListValue, 'prf_algorithm', _('PRF Algorithm'), @@ -489,9 +620,14 @@ return view.extend({ return true; }; o.optional = true; - o.depends('is_esp', '0'); + o.depends({'is_esp': '0', 'use_custom_proposal': '0'}); addAlgorithms(o, algorithms.prf); - return m.render(); + return m.render().then(function (node) { + if (legacyConfig) + showMigrationOverlay(node); + + return node; + }); } }); diff --git a/applications/luci-app-strongswan-swanctl/root/usr/share/rpcd/acl.d/luci-app-strongswan-swanctl.json b/applications/luci-app-strongswan-swanctl/root/usr/share/rpcd/acl.d/luci-app-strongswan-swanctl.json index 7407183247be..22290c45c005 100644 --- a/applications/luci-app-strongswan-swanctl/root/usr/share/rpcd/acl.d/luci-app-strongswan-swanctl.json +++ b/applications/luci-app-strongswan-swanctl/root/usr/share/rpcd/acl.d/luci-app-strongswan-swanctl.json @@ -3,22 +3,24 @@ "description": "Grant access to luci-app-strongswan-swanctl", "read": { "file": { + "/etc/config/ipsec": [ "list" ], "/usr/sbin/swanmon version": [ "exec" ], "/usr/sbin/swanmon stats": [ "exec" ] }, "ubus": { "luci.swanctl": [ "list-algs", "map-conns" ] }, - "uci": [ "ipsec" ] + "uci": [ "swanctl" ] }, "write": { "file": { + "/etc/init.d/swanctl migrate": [ "exec" ], "/usr/sbin/swanctl --initiate --ike *": [ "exec" ], "/usr/sbin/swanctl --initiate --child *": [ "exec" ], "/usr/sbin/swanctl --terminate --ike *": [ "exec" ], "/usr/sbin/swanctl --terminate --child *": [ "exec" ] }, - "uci": [ "ipsec" ] + "uci": [ "swanctl" ] } } }