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 <lvoegl@tdt.de>
This commit is contained in:
Lukas Voegl
2026-05-11 15:47:52 +02:00
committed by Florian Eckert
parent a65bf6d132
commit 55a4b33aaf
2 changed files with 258 additions and 60 deletions
@@ -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;
},
@@ -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" ]
}
}