From 55a4b33aafe74aeda6984f80d98750286204dd79 Mon Sep 17 00:00:00 2001 From: Lukas Voegl Date: Mon, 11 May 2026 15:47:52 +0200 Subject: [PATCH] luci-app-strongswan-swanctl: merge SAs and connections Merges strongSwan conns and SAs into a detailed table of connections and a table of children for each of them. Signed-off-by: Lukas Voegl --- .../view/strongswan-swanctl/status.js | 311 ++++++++++++++---- .../acl.d/luci-app-strongswan-swanctl.json | 7 + 2 files changed, 258 insertions(+), 60 deletions(-) diff --git a/applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/status.js b/applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/status.js index 40494a0b4c..ea34cbdea0 100644 --- a/applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/status.js +++ b/applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/status.js @@ -48,6 +48,213 @@ function buildKeyValueTable(kvPairs) { return buildTable(rows); } +function mapConnectionSas(conns, sas) { + /* map connections to SAs and connection children to SA children */ + const connMap = new Map(); + const childConnMap = new Map(); + conns.forEach(function (connObject) { + const [connName, conn] = Object.entries(connObject)[0]; + connMap.set(connName, conn); + Object.entries(conn.children).forEach(function ([childName, child]) { + childConnMap.set(childName, child); + }); + }); + + sas.forEach(function (saObject) { + const [saName, sa] = Object.entries(saObject)[0]; + const connection = connMap.get(saName); + if (connection) { + connection.childSa = sa; + } + + Object.entries(sa['child-sas']).forEach(function ([childSaName, childSa]) { + const childConnection = childConnMap.get(childSaName); + if (childConnection) { + childConnection.childSa = childSa; + } + }); + }); + + return conns; +} + +function swanctlCommand(parameters) { + return fs.exec('/usr/sbin/swanctl', parameters) + .catch(e => ui.addNotification(null, E('p', e.message))); +} + +function handleConnectionUp(connectionName) { + return swanctlCommand(['--initiate', '--ike', connectionName]); +} + +function handleConnectionDown(connectionName) { + return swanctlCommand(['--terminate', '--ike', connectionName]); +} + +function handleChildUp(childName) { + return swanctlCommand(['--initiate', '--child', childName]); +} + +function handleChildDown(childName) { + return swanctlCommand(['--terminate', '--child', childName]); +} + +function renderDetailsSection(connection, connectionName) { + const sa = connection.sa; + + return buildSection(_('Details'), buildKeyValueTable([ + [_('Name'), connectionName], + [_('Unique ID'), sa ? sa.uniqueid : ''], + [_('Local Addresses'), connection.local_addrs.join(', ')], + [_('Remote Addresses'), connection.remote_addrs.join(', ')], + [_('Local Port'), connection.local_port], + [_('Remote Port'), connection.remote_port], + [_('Version'), connection.version], + [_('Reauthentication Interval'), _('%d seconds').format(connection.reauth_time)], + [_('Rekeying Interval'), _('%d seconds').format(connection.rekey_time)], + [_('Established'), sa ? formatTime(parseInt(sa.established), 3) : ''] + ])); +} + +function handleChildDetails(childName, child, childSa) { + const modal = buildSection(_('Details'), buildKeyValueTable([ + [_('Name'), childName], + [_('Mode'), child.mode], + [_('Protocol'), childSa ? childSa.protocol : ''], + [_('Local Traffic Selectors'), child['local-ts'].join(', ')], + [_('Remote Traffic Selectors'), child['remote-ts'].join(', ')], + [_('Rekey in'), childSa ? _('%d seconds').format(childSa['rekey-time']) : ''], + [_('Encryption Algorithm'), childSa ? childSa['encr-alg'] : ''], + [_('Encryption Keysize'), childSa ? childSa['encr-keysize'] : ''], + [_('Bytes in'), childSa ? childSa['bytes-in'] : ''], + [_('Bytes out'), childSa ? childSa['bytes-out'] : ''], + [_('Life Time'), childSa ? formatTime(parseInt(childSa['life-time']), 2) : ''], + [_('Install Time'), childSa ? formatTime(parseInt(childSa['install-time']), 2) : ''], + [_('SPI in'), childSa ? childSa['spi-in'] : ''], + [_('SPI out'), childSa ? childSa['spi-out'] : ''] + ])); + + ui.showModal(_('Child Details'), [modal, E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn cbi-button', + 'click': ui.hideModal + }, [_('Dismiss')]) + ])], 'cbi-modal'); +} + +function renderChildTable(children) { + const tableHeaders = [ + [_('Name')], + [_('State')], + [_('Mode')], + [_('Protocol')], + [_('Local Traffic Selectors')], + [_('Remote Traffic Selectors')], + [_('Rekey in')], + [/* details button */], + [/* up/down button */] + ] + const childTableRows = [ + E('tr', { 'class': 'tr table-titles' }, tableHeaders.map( + header => E('th', { 'class': 'th' }, header) + )) + ]; + + Object.entries(children).forEach(([childName, child]) => { + const childSa = child.ChildSa; + const state = childSa ? childSa.state : _('Inactive'); + const isDown = !childSa; + + const tableValues = [ + [childName], + [state], + [child.mode], + [childSa ? childSa.protocol : ''], + [child['local-ts'].join(', ')], + [child['remote-ts'].join(', ')], + [childSa ? _('%d seconds').format(childSa['rekey-time']) : ''], + [ + E('button', { + 'title': _('Details'), + 'class': 'btn cbi-button cbi-button-primary', + 'click': ui.createHandlerFn(null, handleChildDetails, childName, child, childSa) + }, [_('Details')]) + ], + [ + E('button', { + 'title': _('Start'), + 'class': 'btn cbi-button cbi-button-positive', + ...(isDown ? {} : { 'disabled': 'disabled' }), + 'click': ui.createHandlerFn(null, handleChildUp, childName) + }, [_('Start')]), + E('button', { + 'title': _('Stop'), + 'class': 'btn cbi-button cbi-button-negative', + ...(isDown ? { 'disabled': 'disabled' } : {}), + 'click': ui.createHandlerFn(null, handleChildDown, childName) + }, [_('Stop')]) + ] + ]; + + childTableRows.push(E('tr', { 'class': 'tr' }, tableValues.map( + value => E('td', { 'class': 'td' }, value) + ))); + }); + + return E('table', { 'class': 'table' }, childTableRows); +} + +function renderAuthTable(auths) { + const authTableRows = [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, [_('Class')]), + E('th', { 'class': 'th' }, [_('ID')]) + ]) + ]; + + auths.forEach(auth => { + authTableRows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, [auth.class]), + E('td', { 'class': 'td' }, [auth.id || '']) + ])); + }); + + return E('table', { 'class': 'table' }, authTableRows); +} + +function filterConnectionAuths(connection, prefix) { + const auths = Object.entries(connection).filter(([key, value]) => key.startsWith(prefix)); + return auths.map(([key, value]) => value); +} + +function handleConnectionDetails(connection, connectionName) { + const detailSection = renderDetailsSection(connection, connectionName); + const childTable = renderChildTable(connection.children); + const localAuths = filterConnectionAuths(connection, 'local-'); + const remoteAuths = filterConnectionAuths(connection, 'remote-'); + const localAuthTable = renderAuthTable(localAuths); + const remoteAuthTable = renderAuthTable(remoteAuths); + + const modal = E([], [E('div', {}, [ + E('div', { 'class': 'cbi-section', 'data-tab': 'details', 'data-tab-title': _('Connection') }, [ + detailSection, + E('h3', _('Local Auth')), + localAuthTable, + E('h3', _('Remote Auth')), + remoteAuthTable + ]), + E('div', { 'class': 'cbi-section', 'data-tab': 'children', 'data-tab-title': _('Children') }, [childTable]) + ])]); + + ui.tabs.initTabGroup(modal.lastElementChild.childNodes); + ui.showModal(_('Connection Details'), [modal, E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn cbi-button', + 'click': ui.hideModal + }, [_('Dismiss')]) + ])], 'cbi-modal'); +} + function collectErrorMessages(results) { const errorMessages = results.reduce(function (messages, result) { return messages.concat(result.errors.map(function (error) { @@ -64,6 +271,7 @@ return view.extend({ return Promise.all([ fs.exec_direct('/usr/sbin/swanmon', ['version'], 'json'), fs.exec_direct('/usr/sbin/swanmon', ['stats'], 'json'), + fs.exec_direct('/usr/sbin/swanmon', ['list-conns'], 'json'), fs.exec_direct('/usr/sbin/swanmon', ['list-sas'], 'json') ]); }, @@ -92,80 +300,63 @@ return view.extend({ return node; } - const [version, stats, sas] = results.map(function (r) { + const [version, stats, conns, sas] = results.map(function (r) { return r.data; }); const uptimeSeconds = (new Date() - new Date(stats.uptime.since)) / 1000; - const statsSection = buildSection(_('Stats'), buildKeyValueTable([ + const overviewSection = buildSection(_('Overview'), buildKeyValueTable([ [_('Version'), version.version], [_('Uptime'), formatTime(uptimeSeconds, 2)], [_('Daemon'), version.daemon], [_('Active IKE_SAs'), stats.ikesas.total], [_('Half-Open IKE_SAs'), stats.ikesas['half-open']] ])); - firstNode.appendChild(statsSection); + firstNode.appendChild(overviewSection); - const tableRows = sas.map(function (conn) { - const name = Object.keys(conn)[0]; - const data = conn[name]; - const childSas = []; + const connections = mapConnectionSas(conns, sas); + const connectionTableRows = [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('Name')), + E('th', { 'class': 'th' }, _('State')), + E('th', { 'class': 'th' }), /* details button */ + E('th', { 'class': 'th' }) /* up/down button */ + ]) + ]; + connections.forEach(function (connectionObject) { + const [connectionName, connection] = Object.entries(connectionObject)[0]; + const state = connection.sa ? connection.sa.state : _('Inactive'); + const isDown = !connection.sa; - Object.entries(data['child-sas']).forEach(function ([name, data]) { - const table = buildKeyValueTable([ - [_('State'), data.state], - [_('Mode'), data.mode], - [_('Protocol'), data.protocol], - [_('Local Traffic Selectors'), data['local-ts'].join(', ')], - [_('Remote Traffic Selectors'), data['remote-ts'].join(', ')], - [_('Encryption Algorithm'), data['encr-alg']], - [_('Encryption Keysize'), data['encr-keysize']], - [_('Bytes in'), data['bytes-in']], - [_('Bytes out'), data['bytes-out']], - [_('Life Time'), formatTime(data['life-time'], 2)], - [_('Install Time'), formatTime(data['install-time'], 2)], - [_('Rekey in'), formatTime(data['rekey-time'], 2)], - [_('SPI in'), data['spi-in']], - [_('SPI out'), data['spi-out']] - ]); - childSas.push(E('div', { 'class': 'cbi-section' }, [ - E('h4', { 'style': 'margin-top: 0; padding-top: 0;' }, [name]), - table - ])); - }); - childSas.push(E('button', { - 'class': 'btn cbi-button cbi-button-apply', - 'click': ui.hideModal - }, _('Close'))); - - return E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, [name]), - E('td', { 'class': 'td' }, [data.state]), - E('td', { 'class': 'td' }, [data['remote-host']]), - E('td', { 'class': 'td' }, [data.version]), - E('td', { 'class': 'td' }, [formatTime(data.established, 2)]), - E('td', { 'class': 'td' }, [formatTime(data['reauth-time'], 2)]), - E('td', { 'class': 'td' }, [E('button', { - 'class': 'btn cbi-button cbi-button-apply', - 'click': function (ev) { - ui.showModal(_('CHILD_SAs'), childSas) - } - }, _('Show Details'))]) - ]); + connectionTableRows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, [connectionName]), + E('td', { 'class': 'td' }, [state]), + E('td', { 'class': 'td', 'width': '20%' }, [E('button', { + 'title': _('Details'), + 'class': 'btn cbi-button cbi-button-primary', + 'click': ui.createHandlerFn(null, handleConnectionDetails, connection, connectionName) + }, [_('Details')])]), + E('td', { 'class': 'td', 'width': '25%' }, [ + E('button', { + 'title': _('Start'), + 'class': 'btn cbi-button cbi-button-positive', + ...(isDown ? {} : { 'disabled': 'disabled' }), + 'click': ui.createHandlerFn(null, handleConnectionUp, connectionName) + }, [_('Start')]), + E('button', { + 'title': _('Stop'), + 'class': 'btn cbi-button cbi-button-negative', + ...(isDown ? { 'disabled': 'disabled' } : {}), + 'click': ui.createHandlerFn(null, handleConnectionDown, connectionName) + }, [_('Stop')]) + ]) + ])); }); - const connSection = buildSection(_('Security Associations (SAs)'), buildTable([ - E('tr', { 'class': 'tr' }, [ - E('th', { 'class': 'th' }, [_('Name')]), - E('th', { 'class': 'th' }, [_('State')]), - E('th', { 'class': 'th' }, [_('Remote')]), - E('th', { 'class': 'th' }, [_('IKE Version')]), - E('th', { 'class': 'th' }, [_('Established for')]), - E('th', { 'class': 'th' }, [_('Reauthentication in')]), - E('th', { 'class': 'th' }, [_('Details')]) - ]), - ...tableRows + + firstNode.appendChild(E([ + E('h2', _('Connections')), + E('table', { 'class': 'table' }, connectionTableRows) ])); - firstNode.appendChild(connSection); 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 d3b44a27a2..2dbed7bcd2 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 @@ -5,11 +5,18 @@ "file": { "/usr/sbin/swanmon version": [ "exec" ], "/usr/sbin/swanmon stats": [ "exec" ], + "/usr/sbin/swanmon list-conns": [ "exec" ], "/usr/sbin/swanmon list-sas": [ "exec" ] }, "uci": [ "ipsec" ] }, "write": { + "file": { + "/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" ] } }