luci-app-strongswan-swanctl: merge SAs and connections
authorLukas Voegl <redacted>
Mon, 11 May 2026 13:47:52 +0000 (15:47 +0200)
committerFlorian Eckert <redacted>
Wed, 13 May 2026 05:37:33 +0000 (07:37 +0200)
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 <redacted>
applications/luci-app-strongswan-swanctl/htdocs/luci-static/resources/view/strongswan-swanctl/status.js
applications/luci-app-strongswan-swanctl/root/usr/share/rpcd/acl.d/luci-app-strongswan-swanctl.json

index 40494a0b4c559b0f22a83d05f7e67fcf609c519e..ea34cbdea0a2b62ce077156a7b4cca2324c24ca2 100644 (file)
@@ -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);
-
-               const tableRows = sas.map(function (conn) {
-                       const name = Object.keys(conn)[0];
-                       const data = conn[name];
-                       const childSas = [];
-
-                       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'))])
-                       ]);
+               firstNode.appendChild(overviewSection);
+
+               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;
+
+                       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;
        },
index d3b44a27a298a4ad8a4bb892aada52a350377d3b..2dbed7bcd24410ef49c9ed0de44f3b5cd4d3516c 100644 (file)
@@ -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" ]
                }
        }
git clone https://git.99rst.org/PROJECT