'use strict';
'require view';
'require form';
'require rpc';
'require uci';
'require ui';
'require fs';
const callListAlgorithms = rpc.declare({
object: 'luci.swanctl',
method: 'list-algs',
expect: { }
});
function validateTimeFormat(section_id, value) {
if (value && !value.match(/^\d+[smhd]$/)) {
return _('Number must have suffix s, m, h or d');
}
return true;
}
function addAlgorithms(o, algorithms) {
algorithms?.forEach(function (algorithm) {
const { name: name, insecure: insecure } = algorithm;
if (insecure) {
o.value(name, '%s*'.format(name));
} else {
o.value(name);
}
});
}
function sectionNameCheck(extra_class) {
var el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments),
nameEl = el.querySelector('.cbi-section-create-name');
ui.addValidator(nameEl, 'uciname', true, function(v) {
let sections = [
...uci.sections('ipsec', 'connection'),
...uci.sections('ipsec', 'child'),
...uci.sections('ipsec', 'crypto_proposal'),
...uci.sections('ipsec', 'local'),
...uci.sections('ipsec', 'remote'),
];
if (sections.find(function(s) {
return s['.name'] == v;
})) {
return _('Connections, Encryption Proposals, Children, Locals and Remotes may not share the same names.') + ' ' +
_('Use combinations like child1_phase1.');
}
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 to the new swanctl uci schema in ' +
'\'/etc/config/ipsec\' before it can be managed here.')),
E('p', _('Before 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.'))
]);
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', L.env.sessionid]).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: function () {
return Promise.all([
callListAlgorithms(),
L.resolveDefault(uci.load('network'), null),
L.resolveDefault(uci.load('ipsec'), null),
]).then(function (data) {
let hasNoGlobals = uci.sections('ipsec', 'globals').length === 0;
return {
algorithms: data[0],
legacyConfig: hasNoGlobals,
};
});
},
render: function (result) {
let m, s, o;
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'),
_('On this page, you can configure the IPsec connections.'));
m.tabbed = true;
// Connection Configuration
s = m.section(form.GridSection, 'connection', _('Connection'),
_('Define Connection IKE Configurations.'));
s.addremove = true;
s.nodescriptions = true;
s.renderSectionAdd = sectionNameCheck
o = s.tab('general', _('General'));
o = s.tab('authentication', _('Authentication'));
o = s.tab('advanced', _('Advanced'));
o = s.taboption('general', form.Flag, 'enabled', _('Enabled'),
_('Configuration is enabled or not'));
o.rmempty = false;
o = s.taboption('general', form.DynamicList, 'remote_addrs', _('Remote Endpoints'),
_('IP address or FQDN name of the tunnel remote endpoints.') + ' ' +
_('If no value is specified, "%any" is assumed.'));
o.datatype = 'or(hostname,ipaddr)';
o.placeholder = '%any';
o = s.taboption('general', form.DynamicList, 'local_addrs', _('Local Endpoints'),
_('IP address or FQDN name of the tunnel local endpoints.') + ' ' +
_('If no value is specified, "%any" is assumed.'));
o.datatype = 'or(hostname,ipaddr)';
o.placeholder = '%any';
o.modalonly = true;
o = s.taboption('general', form.DynamicList, 'vips', _('Virtual IP addresses'),
_('Virtual IP addresses used as the source IP for outgoing traffic.'));
o.datatype = 'ipaddr';
o.modalonly = true;
o = s.taboption('general', form.MultiValue, 'crypto_proposal', _('Crypto Proposal'),
_('List of IKE (phase 1) proposals to use for authentication'));
o.load = function (section_id) {
this.keylist = [];
this.vallist = [];
var sections = uci.sections('ipsec', 'crypto_proposal').filter(function (section) {
return section.is_esp != '1';
});
if (sections.length == 0) {
this.value('', _('Please create a Proposal first'));
} else {
sections.forEach(L.bind(function (section) {
this.value(section['.name']);
}, this));
}
return this.super('load', [section_id]);
};
o.rmempty = false;
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', 'child');
if (sections.length == 0) {
this.value('', _('Please create a Children first'));
} else {
sections.forEach(L.bind(function (section) {
this.value(section['.name'], '%s (%s)'.format(section['.name'], section['mode']));
}, this));
}
return this.super('load', [section_id]);
};
o.rmempty = false;
o = s.taboption('authentication', form.MultiValue, 'local', _('Local Authentication'),
_('Local authentication sections used for this connection.') + ' ' +
_('Each section corresponds to one authentication round.'));
o.load = function (section_id) {
this.keylist = [];
this.vallist = [];
var sections = uci.sections('ipsec', 'local');
if (sections.length == 0) {
this.value('', _('Please create a Local section first'));
} else {
sections.forEach(L.bind(function (section) {
this.value(section['.name']);
}, this));
}
return this.super('load', [section_id]);
};
o = s.taboption('authentication', form.MultiValue, 'remote', _('Remote Authentication'),
_('Remote authentication sections used for this connection.') + ' ' +
_('Each section corresponds to one authentication round.'));
o.load = function (section_id) {
this.keylist = [];
this.vallist = [];
var sections = uci.sections('ipsec', 'remote');
if (sections.length == 0) {
this.value('', _('Please create a Remote section first'));
} else {
sections.forEach(L.bind(function (section) {
this.value(section['.name']);
}, this));
}
return this.super('load', [section_id]);
};
o.rmempty = false;
o = s.taboption('authentication', form.ListValue, 'send_cert', _('Send Certificate'),
_('Whether to send our own certificate to the remote peer'));
o.value('always');
o.value('ifasked');
o.value('never');
o.default = 'ifasked';
o.optional = true;
o.modalonly = true;
o = s.taboption('authentication', form.Flag, 'send_certreq', _('Send Certificate Request'),
_('Send certificate request payloads to offer trusted root CA certificates to the peer'));
o.default = '1';
o.modalonly = true;
o = s.taboption('advanced', form.Flag, 'mobike', _('MOBIKE'),
_('MOBIKE (IKEv2 Mobility and Multihoming Protocol)'));
o.default = '1';
o.modalonly = true;
o.depends('keyexchange', 'ikev2');
o.depends('keyexchange', 'ike');
o = s.taboption('advanced', form.ListValue, 'fragmentation', _('IKE Fragmentation'),
_('Use IKE fragmentation'));
o.value('yes');
o.value('no');
o.value('force');
o.value('accept');
o.default = 'yes';
o.modalonly = true;
o = s.taboption('advanced', form.Value, 'keyingtries', _('Keying Retries'),
_('Number of retransmissions attempts during initial negotiation'));
o.datatype = 'or(uinteger, "%forever")';
o.placeholder = '3';
o.modalonly = true;
o = s.taboption('advanced', form.Value, 'dpd_delay', _('DPD Delay'),
_('Interval to check liveness of a peer'));
o.validate = validateTimeFormat;
o.placeholder = '30s';
o.modalonly = true;
o = s.taboption('advanced', form.Value, 'inactivity', _('Inactivity'),
_('Interval before closing an inactive CHILD_SA'));
o.validate = validateTimeFormat;
o.placeholder = '0s';
o.modalonly = true;
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, 'over_time', _('Overtime'),
_('Limit on time to complete rekeying/reauthentication'));
o.validate = validateTimeFormat;
o.modalonly = true;
o = s.taboption('advanced', form.Flag, 'encap', _('ESP Encapsulation'),
_('To enforce UDP encapsulation of ESP packets, the IKE daemon can manipulate the NAT detection payloads.') + '
' +
_('This makes the peer believe that a NAT situation exist on the transmission path, forcing it to encapsulate ESP packets in UDP.') + '
' +
_('Usually this is not required but it can help to work around connectivity issues with too restrictive intermediary firewalls that block ESP packets.'));
o.modalonly = true;
o.default = '0';
o.rmempty = true;
o = s.taboption('advanced', form.ListValue, 'keyexchange', _('Keyexchange'),
_('Version of IKE for negotiation'));
o.value('ikev1', 'IKEv1 (%s)'.format(_('deprecated')));
o.value('ikev2', 'IKEv2');
o.value('ike', 'IKE (%s, %s)'.format(_('both'), _('deprecated')));
o.default = 'ikev2';
o.modalonly = true;
// Local Configuration
s = m.section(form.GridSection, 'local', _('Local'),
_('Define local IKE authentication rounds referenced from connections.'));
s.addremove = true;
s.nodescriptions = true;
s.renderSectionAdd = sectionNameCheck;
o = s.option(form.ListValue, 'auth', _('Authentication Method'),
_('IKE authentication (phase 1)'));
o.value('psk', 'Pre-shared Key');
o.value('pubkey', 'Public Key');
o.default = 'psk';
o.rmempty = false;
o = s.option(form.Value, 'id', _('Local Identifier'),
_('Local identifier for IKE (phase 1)'));
o.datatype = 'string';
o.placeholder = 'C=US, O=Acme Corporation, CN=headquarters';
o.modalonly = true;
o = s.option(form.Value, 'certs', _('Local Certificate'),
_('Certificate to use for authentication, relative to /etc/swanctl/x509.'));
o.datatype = 'file';
o.modalonly = true;
o = s.option(form.Value, 'key', _('Local Key'),
_('Private key to use with the certificate, relative to /etc/swanctl/private.'));
o.datatype = 'file';
o.modalonly = true;
// Remote Configuration
s = m.section(form.GridSection, 'remote', _('Remote'),
_('Define remote IKE authentication rounds referenced from connections.'));
s.addremove = true;
s.nodescriptions = true;
s.renderSectionAdd = sectionNameCheck;
o = s.option(form.ListValue, 'auth', _('Authentication Method'),
_('IKE authentication (phase 1)'));
o.value('psk', 'Pre-shared Key');
o.value('pubkey', 'Public Key');
o.value('eap-mschapv2', 'EAP MSCHAPv2');
o.value('eap-tls', 'EAP TLS');
o.default = 'psk';
o.rmempty = false;
o = s.option(form.Value, 'id', _('Remote Identifier'),
_('Remote identifier for IKE (phase 1)'));
o.datatype = 'string';
o.placeholder = 'C=US, O=Acme Corporation, CN=soho';
o.modalonly = true;
o = s.option(form.DynamicList, 'cacerts', _('Remote CA Certificates'),
_('Restrict the remote peer\'s certificate to be issued by one of these CAs'));
o.datatype = 'file';
o.modalonly = true;
o = s.option(form.Value, 'eap_id', _('EAP ID'),
_('EAP identity to use with the remote peer'));
o.datatype = 'string';
o.default = '%any';
o.depends('auth', 'eap-mschapv2');
o.depends('auth', 'eap-tls');
o.modalonly = true;
// Children Configuration
s = m.section(form.GridSection, 'child', _('Children'),
_('Define Connection Children to be used in Remote Configurations.'));
s.addremove = true;
s.nodescriptions = true;
s.renderSectionAdd = sectionNameCheck;
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_ts', _('Local Traffic Selectors'),
_('Local network(s)'));
o.datatype = 'cidr';
o.placeholder = '192.168.1.1/24';
o.rmempty = false;
o = s.taboption('general', form.DynamicList, 'remote_ts', _('Remote Traffic Selectors'),
_('Remote network(s)'));
o.datatype = 'cidr';
o.placeholder = '192.168.2.1/24';
o.rmempty = false;
o = s.taboption('general', form.ListValue, 'if_id', ('XFRM Interface ID'),
_('XFRM interface ID set on input and output interfaces'));
o.load = function (section_id) {
this.keylist = [];
this.vallist = [];
var xfrmSections = uci.sections('network').filter(function (section) {
return section.proto == 'xfrm';
});
xfrmSections.forEach(L.bind(function (section) {
this.value(section.ifid,
'%s (%s)'.format(section.ifid, section['.name']));
}, this));
return this.super('load', [section_id]);
}
o.optional = true;
o.modalonly = true;
o = s.taboption('general', form.ListValue, 'start_action', _('Start Action'),
_('Action on initial configuration load'));
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.ListValue, 'close_action', _('Close Action'),
_('Action when CHILD_SA is closed'));
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',
_('Crypto Proposal (Phase 2)'),
_('List of ESP (phase two) proposals. Only Proposals with checked ESP flag are selectable'));
o.load = function (section_id) {
this.keylist = [];
this.vallist = [];
var sections = uci.sections('ipsec', 'crypto_proposal').filter(function (section) {
return section.is_esp == '1';
});
if (sections.length == 0) {
this.value('', _('Please create an ESP Proposal first'));
} else {
sections.forEach(L.bind(function (section) {
this.value(section['.name']);
}, this));
}
return this.super('load', [section_id]);
};
o.rmempty = false;
o = s.taboption('advanced', form.Value, 'updown', _('Up/Down Script Path'),
_('Path to script to run on CHILD_SA up/down events'));
o.datatype = 'file';
o.modalonly = true;
o = s.taboption('advanced', form.ListValue, 'dpd_action', _('DPD Action'),
_('Action when DPD timeout occurs'));
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, '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".')
);
o.placeholder = '1h';
o.validate = validateTimeFormat;
o.rmempty = true;
o.modalonly = true;
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;
o.rmempty = true;
o.modalonly = true;
o = s.taboption('advanced', form.Flag, 'ipcomp', _('IPComp'),
_('Enable ipcomp compression'));
o.default = '0';
o.modalonly = true;
o = s.taboption('advanced', form.ListValue, 'hw_offload', _('H/W Offload'),
_('Enable Hardware offload'));
o.value('yes');
o.value('no');
o.value('auto');
o.optional = true;
o.modalonly = true;
o = s.taboption('advanced', form.Value, 'priority', _('Priority'),
_('Priority of the CHILD_SA'));
o.datatype = 'uinteger';
o.modalonly = true;
o = s.taboption('advanced', form.Value, 'replay_window', _('Replay Window'),
'%s; %s'.format(_('Replay Window of the CHILD_SA'),
_('Values larger than 32 are supported by the Netlink backend only')));
o.datatype = 'uinteger';
o.modalonly = true;
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.')
);
o.datatype = 'uinteger';
o.placeholder = '0';
o.rmempty = true;
o.modalonly = true;
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).')
);
o.datatype = 'uinteger';
o.placeholder = '0';
o.rmempty = true;
o.modalonly = true;
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).')
);
o.datatype = 'uinteger';
o.placeholder = '0';
o.rmempty = true;
o.modalonly = true;
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).')
);
o.datatype = 'uinteger';
o.placeholder = '0';
o.rmempty = true;
o.modalonly = true;
// Crypto Proposals
s = m.section(form.GridSection, 'crypto_proposal',
_('Encryption Proposals'),
_('Configure Cipher Suites to define IKE (Phase 1) or ESP (Phase 2) Proposals.'));
s.addremove = true;
s.nodescriptions = true;
s.renderSectionAdd = sectionNameCheck;
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, 'use_custom_proposal': '0'});
});
o.default = 'sha512';
o.rmempty = false;
addAlgorithms(o, algorithms.integrity);
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'),
_('Algorithms marked with * are considered insecure'));
o.validate = function (section_id, value) {
const encryptionAlgorithm = this.section.formvalue(section_id, 'encryption_algorithm');
const aeadAlgorithmNames = algorithms.aead?.map(algorithm => algorithm.name);
if (aeadAlgorithmNames?.includes(encryptionAlgorithm) && !value) {
return _('PRF Algorithm must be configured when using an Authenticated Encryption Algorithm');
}
return true;
};
o.optional = true;
o.depends({'is_esp': '0', 'use_custom_proposal': '0'});
addAlgorithms(o, algorithms.prf);
return m.render().then(function (node) {
if (legacyConfig)
showMigrationOverlay(node);
return node;
});
}
});