VPN/IPsec add new MVC module (#6187)

Add new component to manage IPsec connections in a similar format as `swanctl.conf` is defined (https://docs.strongswan.org/docs/5.9/swanctl/swanctlConf.html).  As this needs to work in conjunction with the legacy IPsec module, some minor changes are needed to the current state. 

o VPN/IPsec/Pre-Shared Keys - add optional remote identifier (merges in `ipsec.inc`)
o VPN/IPsec/Virtual Tunnel Interfaces - new component to show existing VTI's and add new ones (as these are separate entities)
o VPN/IPsec/Connections [new] - configuration tool to build `swanctl.conf` 
o Integrate MVC generated `swanctl.conf` into `ipsec.inc` (legacy overlays)
o Integrate manually configured VTI's into `ipsec.inc` (`array_merge(ipsec_get_configured_vtis(), (new \OPNsense\IPsec\Swanctl())->getVtiDevices())`)
o fix minor php warning when changing reqid's (`$local|remote_configured` initialisation when `$configured_intf[$intf]` not found)
This commit is contained in:
Ad Schellevis 2022-12-12 10:37:43 +01:00 committed by GitHub
parent d25318a483
commit 5752bd6eb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2568 additions and 18 deletions

View File

@ -176,7 +176,8 @@ function ipsec_interfaces()
}
}
foreach (ipsec_get_configured_vtis() as $intf => $details) {
$vtis = array_merge(ipsec_get_configured_vtis(), (new \OPNsense\IPsec\Swanctl())->getVtiDevices());
foreach ($vtis as $intf => $details) {
$interfaces[$intf] = [
'enable' => true,
'descr' => preg_replace('/[^a-z_0-9]/i', '', $details['descr']),
@ -194,7 +195,8 @@ function ipsec_devices()
$devices = [];
$names = [];
foreach (ipsec_get_configured_vtis() as $device => $details) {
$vtis = array_merge(ipsec_get_configured_vtis(), (new \OPNsense\IPsec\Swanctl())->getVtiDevices());
foreach ($vtis as $device => $details) {
$names[$device] = [
'descr' => sprintf('%s (%s)', $device, $details['descr']),
'ifdescr' => sprintf('%s', $details['descr']),
@ -1199,10 +1201,12 @@ function ipsec_write_secrets()
foreach ((new \OPNsense\IPsec\IPsec())->preSharedKeys->preSharedKey->iterateItems() as $uuid => $psk) {
$keytype = strtolower($psk->keyType);
$secrets["{$keytype}-{$uuid}"] = [
'id-0' => (string)$psk->ident,
'secret' => '0s' . base64_encode((string)$psk->Key)
];
$dataKey = "{$keytype}-{$uuid}";
$secrets[$dataKey] = ['id-0' => (string)$psk->ident];
if (!empty((string)$psk->remote_ident)) {
$secrets[$dataKey]['id-1'] = (string)$psk->remote_ident;
}
$secrets[$dataKey]['secret'] = '0s' . base64_encode((string)$psk->Key);
}
return $secrets;
@ -1272,12 +1276,10 @@ function ipsec_configure_do($verbose = false, $interface = '')
ipsec_write_certs();
ipsec_write_keypairs();
/* begin ipsec.conf */
$swanctl = [
'connections' => [],
'pools' => [],
'secrets' => ipsec_write_secrets()
];
/* begin ipsec.conf, hook mvc configuration first */
$swanctl = (new \OPNsense\IPsec\Swanctl())->getConfig();
$swanctl['secrets'] = ipsec_write_secrets();
if (count($a_phase1)) {
if (!empty($config['ipsec']['passthrough_networks'])) {
$swanctl['connections']['pass'] = [
@ -1652,7 +1654,7 @@ function ipsec_get_configured_vtis()
function ipsec_configure_vti($verbose = false, $device = null)
{
// query planned and configured interfaces
$configured_intf = ipsec_get_configured_vtis();
$configured_intf = array_merge(ipsec_get_configured_vtis(), (new \OPNsense\IPsec\Swanctl())->getVtiDevices());
$current_interfaces = [];
foreach (legacy_interfaces_details() as $intf => $intf_details) {
@ -1669,14 +1671,18 @@ function ipsec_configure_vti($verbose = false, $device = null)
continue;
}
$local_configured = $configured_intf[$intf]['local'];
$remote_configured = $configured_intf[$intf]['remote'];
$local_configured = null;
$remote_configured = null;
if (!empty($configured_intf[$intf])) {
if (!is_ipaddr($configured_intf[$intf]['local'])) {
$local_configured = ipsec_resolve($configured_intf[$intf]['local']);
} else {
$local_configured = $configured_intf[$intf]['local'];
}
if (!is_ipaddr($configured_intf[$intf]['remote'])) {
$remote_configured = ipsec_resolve($configured_intf[$intf]['remote']);
} else {
$remote_configured = $configured_intf[$intf]['remote'];
}
}
if (

View File

@ -0,0 +1,278 @@
<?php
/*
* Copyright (C) 2022 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\IPsec\Api;
use OPNsense\Base\ApiMutableModelControllerBase;
/**
* Class ConnectionsController
* @package OPNsense\IPsec\Api
*/
class ConnectionsController extends ApiMutableModelControllerBase
{
protected static $internalModelName = 'swanctl';
protected static $internalModelClass = 'OPNsense\IPsec\Swanctl';
/**
* @return null|function lambda to filter on provided connection uuid in GET['connection']
*/
private function connectionFilter()
{
$connection = $this->request->get('connection');
$filter_func = null;
if (!empty($connection)) {
$filter_func = function ($record) use ($connection) {
return $record->connection == $connection;
};
}
return $filter_func;
}
/**
* @param array $payload result array
* @param string $topic topic used as root container
* @return array $payload with optional preselected connection defaults (to be used by children of connection)
*/
private function wrapDefaults($payload, $topic)
{
$conn_uuid = $this->request->get('connection');
if (!empty($conn_uuid)) {
foreach ($payload[$topic]['connection'] as $key => &$value) {
if ($key == $conn_uuid) {
$value['selected'] = 1;
} else {
$value['selected'] = 0;
}
}
}
return $payload;
}
public function searchConnectionAction()
{
return $this->searchBase(
'Connections.Connection',
['description', 'enabled', 'local_addrs', 'remote_addrs', 'local_ts', 'remote_ts']
);
}
public function setConnectionAction($uuid = null)
{
$copy_uuid = null;
$post = $this->request->getPost('connection');
if (empty($uuid) && !empty($post) && !empty($post['uuid'])) {
// use form provided uuid when not provided as uri parameter
$uuid = $post['uuid'];
$copy_uuid = $post['org_uuid'] ?? null;
}
$result = $this->setBase('connection', 'Connections.Connection', $uuid);
// copy children (when none exist)
if (!empty($copy_uuid) && $result['result'] != 'failed') {
$changed = False;
foreach (['locals.local', 'remotes.remote', 'children.child'] as $ref) {
$container = $this->getModel()->getNodeByReference($ref);
if ($container != null) {
$orignal_items = [];
$has_children = False;
foreach ($container->iterateItems() as $node_uuid => $node) {
if ($node->connection == $copy_uuid) {
$record = [];
foreach ($node->iterateItems() as $key => $field) {
$record[$key] = (string)$field;
}
$orignal_items[] = $record;
} elseif ($node->connection == $uuid) {
$has_children = True;
}
}
if (!$has_children) {
foreach ($orignal_items as $record) {
$node = $container->Add();
$record['connection'] = $uuid;
$node->setNodes($record);
$changed = True;
}
}
}
}
if ($changed) {
$this->save();
}
}
return $result;
}
public function addConnectionAction()
{
return $this->addBase('connection', 'Connections.Connection');
}
public function getConnectionAction($uuid = null)
{
$result = $this->getBase('connection', 'Connections.Connection', $uuid);
if (!empty($result['connection'])) {
$fetchmode = $this->request->has("fetchmode") ? $this->request->get("fetchmode") : null;
$result['connection']['org_uuid'] = $uuid;
if (empty($uuid) || $fetchmode == 'copy') {
$result['connection']['uuid'] = $this->getModel()->Connections->generateUUID();
} else {
$result['connection']['uuid'] = $uuid;
}
}
return $result;
}
public function toggleConnectionAction($uuid, $enabled = null)
{
return $this->toggleBase('Connections.Connection', $uuid, $enabled);
}
public function connectionExistsAction($uuid)
{
return [
"exists" => isset($this->getModel()->Connections->Connection->$uuid)
];
}
public function delConnectionAction($uuid)
{
// remove children
foreach (['locals.local', 'remotes.remote', 'children.child'] as $ref) {
$tmp = $this->getModel()->getNodeByReference($ref);
if ($tmp != null) {
foreach ($tmp->iterateItems() as $node_uuid => $node) {
if ($node->connection == $uuid) {
$this->delBase($ref, $node_uuid);
}
}
}
}
return $this->delBase('Connections.Connection', $uuid);
}
public function searchLocalAction()
{
return $this->searchBase(
'locals.local',
['description', 'round', 'auth', 'enabled'],
'description',
$this->connectionFilter()
);
}
public function getLocalAction($uuid = null)
{
return $this->wrapDefaults(
$this->getBase('local', 'locals.local', $uuid),
'local'
);
}
public function setLocalAction($uuid = null)
{
return $this->setBase('local', 'locals.local', $uuid);
}
public function addLocalAction()
{
return $this->addBase('local', 'locals.local');
}
public function toggleLocalAction($uuid, $enabled = null)
{
return $this->toggleBase('locals.local', $uuid, $enabled);
}
public function delLocalAction($uuid)
{
return $this->delBase('locals.local', $uuid);
}
public function searchRemoteAction()
{
return $this->searchBase(
'remotes.remote',
['description', 'round', 'auth', 'enabled'],
'description',
$this->connectionFilter()
);
}
public function getRemoteAction($uuid = null)
{
return $this->wrapDefaults(
$this->getBase('remote', 'remotes.remote', $uuid),
'remote'
);
}
public function setRemoteAction($uuid = null)
{
return $this->setBase('remote', 'remotes.remote', $uuid);
}
public function addRemoteAction()
{
return $this->addBase('remote', 'remotes.remote');
}
public function toggleRemoteAction($uuid, $enabled = null)
{
return $this->toggleBase('remotes.remote', $uuid, $enabled);
}
public function delRemoteAction($uuid)
{
return $this->delBase('remotes.remote', $uuid);
}
public function searchChildAction()
{
return $this->searchBase(
'children.child',
['description', 'enabled', 'local_ts', 'remote_ts'],
'description',
$this->connectionFilter()
);
}
public function getChildAction($uuid = null)
{
return $this->wrapDefaults(
$this->getBase('child', 'children.child', $uuid),
'child'
);
}
public function setChildAction($uuid = null)
{
return $this->setBase('child', 'children.child', $uuid);
}
public function addChildAction()
{
return $this->addBase('child', 'children.child');
}
public function toggleChildAction($uuid, $enabled = null)
{
return $this->toggleBase('children.child', $uuid, $enabled);
}
public function delChildAction($uuid)
{
return $this->delBase('children.child', $uuid);
}
}

View File

@ -0,0 +1,71 @@
<?php
/*
* Copyright (C) 2022 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\IPsec\Api;
use OPNsense\Base\ApiMutableModelControllerBase;
/**
* Class PoolsController
* @package OPNsense\IPsec\Api
*/
class PoolsController extends ApiMutableModelControllerBase
{
protected static $internalModelName = 'swanctl';
protected static $internalModelClass = 'OPNsense\IPsec\Swanctl';
public function searchAction()
{
return $this->searchBase('Pools.Pool', ['name', 'enabled']);
}
public function setAction($uuid = null)
{
return $this->setBase('pool', 'Pools.Pool', $uuid);
}
public function addAction()
{
return $this->addBase('pool', 'Pools.Pool');
}
public function getAction($uuid = null)
{
return $this->getBase('pool', 'Pools.Pool', $uuid);
}
public function toggleAction($uuid, $enabled = null)
{
return $this->toggleBase('Pools.Pool', $uuid, $enabled);
}
public function delAction($uuid)
{
return $this->delBase('Pools.Pool', $uuid);
}
}

View File

@ -46,7 +46,7 @@ class PreSharedKeysController extends ApiMutableModelControllerBase
*/
public function searchItemAction()
{
return $this->searchBase('preSharedKeys.preSharedKey', ['ident', 'keyType']);
return $this->searchBase('preSharedKeys.preSharedKey', ['ident', 'remote_ident', 'keyType']);
}
/**

View File

@ -0,0 +1,74 @@
<?php
/*
* Copyright (C) 2022 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\IPsec\Api;
use OPNsense\Base\ApiMutableModelControllerBase;
/**
* Class VtiController
* @package OPNsense\IPsec\Api
*/
class VtiController extends ApiMutableModelControllerBase
{
protected static $internalModelName = 'swanctl';
protected static $internalModelClass = 'OPNsense\IPsec\Swanctl';
public function searchAction()
{
return $this->searchBase(
'VTIs.VTI',
['enabled', 'description', 'origin', 'reqid', 'local', 'remote', 'tunnel_local', 'tunnel_remote']
);
}
public function setAction($uuid = null)
{
return $this->setBase('vti', 'VTIs.VTI', $uuid);
}
public function addAction()
{
return $this->addBase('vti', 'VTIs.VTI');
}
public function getAction($uuid = null)
{
return $this->getBase('vti', 'VTIs.VTI', $uuid);
}
public function toggleAction($uuid, $enabled = null)
{
return $this->toggleBase('VTIs.VTI', $uuid, $enabled);
}
public function delAction($uuid)
{
return $this->delBase('VTIs.VTI', $uuid);
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* Copyright (C) 2022 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\IPsec;
class ConnectionsController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/IPsec/connections');
$this->view->formDialogConnection = $this->getForm('dialogConnection');
$this->view->formDialogLocal = $this->getForm('dialogLocal');
$this->view->formDialogRemote = $this->getForm('dialogRemote');
$this->view->formDialogChild = $this->getForm('dialogChild');
$this->view->formDialogPool = $this->getForm('dialogPool');
}
}

View File

@ -0,0 +1,38 @@
<?php
/*
* Copyright (C) 2022 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\IPsec;
class VtiController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/IPsec/vti');
$this->view->formDialogVTI = $this->getForm('dialogVTI');
}
}

View File

@ -0,0 +1,132 @@
<form>
<field>
<id>child.enabled</id>
<label>enabled</label>
<type>checkbox</type>
</field>
<field>
<id>child.connection</id>
<label>Connection</label>
<type>dropdown</type>
</field>
<field>
<id>child.sha256_96</id>
<label>sha256_96</label>
<type>checkbox</type>
<help>
HMAC-SHA-256 is used with 128-bit truncation with IPsec.
For compatibility with implementations that incorrectly use 96-bit truncation this option may be enabled to
configure the shorter truncation length in the kernel.
This is not negotiated, so this only works with peers that use the incorrect truncation length (or have this option enabled)
</help>
<advanced>true</advanced>
</field>
<field>
<id>child.mode</id>
<label>Mode</label>
<type>dropdown</type>
<help>
IPsec Mode to establish CHILD_SA with.
tunnel negotiates the CHILD_SA in IPsec Tunnel Mode whereas transport uses IPsec Transport Mode.
pass and drop are used to install shunt policies which explicitly bypass the defined traffic from IPsec processing or drop it, respectively.
</help>
</field>
<field>
<id>child.policies</id>
<label>Policies</label>
<type>checkbox</type>
<help>
Whether to install IPsec policies or not.
Disabling this can be useful in some scenarios e.g. VTI where policies are not managed by the IKE daemon
</help>
<advanced>true</advanced>
</field>
<field>
<id>child.start_action</id>
<label>Start action</label>
<type>dropdown</type>
<help>
Action to perform after loading the configuration.
The default of none loads the connection only, which then can be manually initiated or used as a responder configuration.
The value trap installs a trap policy which triggers the tunnel as soon as matching traffic has been detected.
The value start initiates the connection actively.
To immediately initiate a connection for which trap policies have been installed, user Trap+start.
</help>
</field>
<field>
<id>child.close_action</id>
<label>Close action</label>
<type>dropdown</type>
<advanced>true</advanced>
<help>
Action to perform after a CHILD_SA gets closed by the peer.
The default of none does not take any action.
trap installs a trap policy for the CHILD_SA (note that this is redundant if start_action includes trap).
start tries to immediately re-create the CHILD_SA.
close_action does not provide any guarantee that the CHILD_SA is kept alive.
It acts on explicit close messages only but not on negotiation failures.
Use trap policies to reliably re-create failed CHILD_SAs
</help>
</field>
<field>
<id>child.dpd_action</id>
<label>DPD action</label>
<type>dropdown</type>
<help>
Action to perform for this CHILD_SA on DPD timeout.
The default clear closes the CHILD_SA and does not take further action.
trap installs a trap policy, which will catch matching traffic and tries to re-negotiate the tunnel on-demand
(note that this is redundant if start_action includes trap.
restart immediately tries to re-negotiate the CHILD_SA under a fresh IKE_SA.
</help>
</field>
<field>
<id>child.reqid</id>
<label>Reqid</label>
<type>text</type>
<help>
This might be helpful in some scenarios, like route based tunnels (VTI), but works only if each CHILD_SA configuration is instantiated not more than once.
The default uses dynamic reqids, allocated incrementally
</help>
</field>
<field>
<id>child.esp_proposals</id>
<label>ESP proposals</label>
<type>select_multiple</type>
</field>
<field>
<id>child.local_ts</id>
<label>Local</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help>List of local traffic selectors to include in CHILD_SA. Each selector is a CIDR subnet definition.</help>
</field>
<field>
<id>child.remote_ts</id>
<label>Remote</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help>List of remote traffic selectors to include in CHILD_SA. Each selector is a CIDR subnet definition.</help>
</field>
<field>
<id>child.rekey_time</id>
<label>Rekey time (s)</label>
<type>text</type>
<help>
Time to schedule CHILD_SA rekeying.
CHILD_SA rekeying refreshes key material, optionally using a Diffie-Hellman exchange if a group is specified in the proposal.
To avoid rekey collisions initiated by both ends simultaneously, a value in the range of rand_time
gets subtracted to form the effective soft lifetime.
By default CHILD_SA rekeying is scheduled every hour, minus rand_time
</help>
<advanced>true</advanced>
</field>
<field>
<id>child.description</id>
<label>Description</label>
<type>text</type>
</field>
</form>

View File

@ -0,0 +1,234 @@
<form>
<field>
<id>connection.uuid</id>
<label>uuid</label>
<type>text</type>
<style>hidden_attr</style>
</field>
<field>
<id>connection.org_uuid</id>
<label>orignal uuid</label>
<type>text</type>
<style>hidden_attr</style>
</field>
<field>
<id>connection.enabled</id>
<label>enabled</label>
<type>checkbox</type>
</field>
<field>
<id>connection.proposals</id>
<label>Proposals</label>
<type>select_multiple</type>
<help>
A proposal is a set of algorithms.
For non-AEAD algorithms this includes IKE an encryption algorithm, an integrity algorithm,
a pseudo random function (PRF) and a Diffie-Hellman key exchange group.
For AEAD algorithms, instead of encryption and integrity algorithms a combined algorithm is used.
With IKEv2 multiple algorithms of the same kind can be specified in a single proposal, from which one gets selected.
For IKEv1 only one algorithm per kind is allowed per proposal, more algorithms get implicitly stripped.
Use multiple proposals to offer different algorithm combinations with IKEv1. Algorithm keywords get separated using dashes.
Multiple proposals may be separated by commas.
The special value default adds a default proposal of supported algorithms considered safe and is usually a good choice for interoperability.
</help>
</field>
<field>
<id>connection.unique</id>
<label>Unique</label>
<type>dropdown</type>
<help>Connection uniqueness policy to enforce.
To avoid multiple connections from the same user, a uniqueness policy can be enforced.
</help>
<advanced>true</advanced>
</field>
<field>
<id>connection.aggressive</id>
<label>Aggressive</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>
Enables IKEv1 Aggressive Mode instead of IKEv1 Main Mode with Identity Protection.
Aggressive Mode is considered less secure because the ID and HASH payloads are exchanged unprotected.
This allows a passive attacker to snoop peer identities and even worse, start dictionary attacks on the Preshared Key
</help>
</field>
<field>
<id>connection.version</id>
<label>Version</label>
<type>dropdown</type>
<help>
IKE major version to use for connection. 1 uses IKEv1 aka ISAKMP, 2 uses IKEv2.
A connection using IKEv1+IKEv2 accepts both IKEv1 and IKEv2 as a responder
and initiates the connection actively with IKEv2
</help>
</field>
<field>
<id>connection.mobike</id>
<label>MOBIKE</label>
<type>checkbox</type>
<help>
Enables MOBIKE on IKEv2 connections.
MOBIKE is enabled by default on IKEv2 connections and allows mobility of clients and multi-homing on servers
by migrating active IPsec tunnels.
Usually keeping MOBIKE enabled is unproblematic, as it is not used if the peer does not indicate support for it.
However, due to the design of MOBIKE, IKEv2 always floats to UDP port 4500 starting from the second exchange.
Some implementations dont like this behavior, hence it can be disabled
</help>
</field>
<field>
<id>connection.local_addrs</id>
<label>Local addresses</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help>
Local address[es] to use for IKE communication.
Accepts single IPv4/IPv6 addresses, DNS names, CIDR subnets or IP address ranges.
As an initiator, the first non-range/non-subnet is used to initiate the connection from.
As a responder the local destination address must match at least to one of the specified addresses, subnets or ranges.
If FQDNs are assigned, they are resolved every time a configuration lookup is done.
If DNS resolution times out, the lookup is delayed for that time. When left empty %any is choosen as default.
</help>
</field>
<field>
<id>connection.remote_addrs</id>
<label>Remote addresses</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help>
Remote address[es] to use for IKE communication.
Accepts single IPv4/IPv6 addresses, DNS names, CIDR subnets or IP address ranges.
As an initiator, the first non-range/non-subnet is used to initiate the connection to.
As a responder, the initiator source address must match at least to one of the specified addresses, subnets or ranges.
If FQDNs are assigned they are resolved every time a configuration lookup is done.
If DNS resolution times out, the lookup is delayed for that time.
To initiate a connection, at least one specific address or DNS name must be specified.
</help>
</field>
<field>
<id>connection.encap</id>
<label>UDP encapsulation</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>
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
</help>
</field>
<field>
<id>connection.reauth_time</id>
<label>Re-auth time (s)</label>
<type>text</type>
<advanced>true</advanced>
<help>
Time to schedule IKE reauthentication.
IKE reauthentication recreates the IKE/ISAKMP SA from scratch and re-evaluates the credentials.
In asymmetric configurations (with EAP or configuration payloads) it might not be possible to actively reauthenticate as responder.
The IKEv2 reauthentication lifetime negotiation can instruct the client to perform reauthentication.
Reauthentication is disabled by default (0).
Enabling it usually may lead to small connection interruptions as strongSwan uses a break-before-make policy with IKEv2 by default.
</help>
</field>
<field>
<id>connection.rekey_time</id>
<label>Rekey time (s)</label>
<type>text</type>
<advanced>true</advanced>
<help>
IKE rekeying refreshes key material using a Diffie-Hellman key exchange, but does not re-check associated credentials.
It is supported with IKEv2 only. IKEv1 performs a reauthentication procedure instead.
With the default value, IKE rekeying is scheduled every 4 hours minus the configured rand_time.
If a reauth_time is configured, rekey_time defaults to zero, disabling rekeying.
In that case set rekey_time explicitly to both enforce rekeying and reauthentication
</help>
</field>
<field>
<id>connection.over_time</id>
<label>Over time (s)</label>
<type>text</type>
<advanced>true</advanced>
<help>
Hard IKE_SA lifetime if rekey/reauth does not complete, as time.
To avoid having an IKE or ISAKMP connection kept alive if IKE reauthentication or rekeying fails perpetually,
a maximum hard lifetime may be specified.
If the IKE_SA fails to rekey or reauthenticate within the specified time, the IKE_SA gets closed.
In contrast to CHILD_SA rekeying, over_time is relative in time to the rekey_time and reauth_time values, as it applies to both.
The default is 10% of either rekey_time or reauth_time, whichever value is larger. [0.1 * max(rekey_time, reauth_time)]
</help>
</field>
<field>
<id>connection.dpd_delay</id>
<label>DPD delay (s)</label>
<type>text</type>
<help>
Interval to check the liveness of a peer actively using IKEv2 INFORMATIONAL exchanges or IKEv1 R_U_THERE messages.
Active DPD checking is only enforced if no IKE or ESP/AH packet has been received for the configured DPD delay. Defaults to 0s
</help>
</field>
<field>
<id>connection.dpd_timeout</id>
<label>DPD timeout (s)</label>
<type>text</type>
<advanced>true</advanced>
<help>
Charon by default uses the normal retransmission mechanism and timeouts to check the liveness of a peer,
as all messages are used for liveness checking.
For compatibility reasons, with IKEv1 a custom interval may be specified.
This option has no effect on IKEv2 connections
</help>
</field>
<field>
<id>connection.pools</id>
<label>Pools</label>
<type>select_multiple</type>
<help>
List of named IP pools to allocate virtual IP addresses and other configuration attributes from.
Each name references a pool by name from either the pools section or an external pool.
Note that the order in which they are queried primarily depends on the plugin order.
</help>
</field>
<field>
<id>connection.send_certreq</id>
<label>Send cert req</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>
Send certificate request payloads to offer trusted root CA certificates to the peer.
Certificate requests help the peer to choose an appropriate certificate/private key for authentication and are enabled by default.
Disabling certificate requests can be useful if too many trusted root CA certificates are installed,
as each certificate request increases the size of the initial IKE packets
</help>
</field>
<field>
<id>connection.send_cert</id>
<label>Send certificate</label>
<type>dropdown</type>
<advanced>true</advanced>
<help>
Send certificate payloads when using certificate authentication.
With the default of [ifasked] the daemon sends certificate payloads only if certificate requests have been received.
[never] disables sending of certificate payloads altogether whereas [always] causes certificate payloads to be sent unconditionally
whenever certificate-based authentication is used.
</help>
</field>
<field>
<id>connection.keyingtries</id>
<label>Keyingtries</label>
<type>text</type>
<advanced>true</advanced>
<help>
Number of retransmission sequences to perform during initial connect.
Instead of giving up initiation after the first retransmission sequence with the default value of 1,
additional sequences may be started according to the configured value.
A value of 0 initiates a new sequence until the connection establishes or fails with a permanent error
</help>
</field>
<field>
<id>connection.description</id>
<label>Description</label>
<type>text</type>
</field>
</form>

View File

@ -0,0 +1,62 @@
<form>
<field>
<id>local.enabled</id>
<label>enabled</label>
<type>checkbox</type>
</field>
<field>
<id>local.connection</id>
<label>Connection</label>
<type>dropdown</type>
</field>
<field>
<id>local.round</id>
<label>Round</label>
<type>text</type>
<help>Numeric identifier by which authentication rounds are sorted.</help>
</field>
<field>
<id>local.auth</id>
<label>Authentication</label>
<type>dropdown</type>
<help>Authentication to perform for this round, when using Pre-Shared key make sure to define one under "VPN->IPsec->Pre-Shared Keys"</help>
</field>
<field>
<id>local.id</id>
<label>Id</label>
<type>text</type>
<help>IKE identity to use for authentication round.
When using certificate authentication.
The IKE identity must be contained in the certificate,
either as the subject DN or as a subjectAltName
(the identity will default to the certificates subject DN if not specified).
Refer to https://docs.strongswan.org/docs/5.9/config/identityParsing.html for details on how
identities are parsed and may be configured.
</help>
</field>
<field>
<id>local.eap_id</id>
<label>EAP Id</label>
<type>text</type>
<help>Client EAP-Identity to use in EAP-Identity exchange and the EAP method</help>
<style>local_auth local_auth_eap-mschapv2 local_auth_eap-tls local_auth_eap-radius</style>
</field>
<field>
<id>local.certs</id>
<label>Certificates</label>
<type>select_multiple</type>
<help>List of certificate candidates to use for authentication.</help>
</field>
<field>
<id>local.pubkeys</id>
<label>Public Keys</label>
<type>select_multiple</type>
<help>List of raw public key candidates to use for authentication.</help>
<style>local_auth local_auth_pubkey</style>
</field>
<field>
<id>local.description</id>
<label>Description</label>
<type>text</type>
</field>
</form>

View File

@ -1,10 +1,16 @@
<form>
<field>
<id>preSharedKey.ident</id>
<label>Identifier</label>
<label>Local Identifier</label>
<type>text</type>
<help>This can be either an IP address, fully qualified domain name or an email address.</help>
</field>
<field>
<id>preSharedKey.remote_ident</id>
<label>Remote Identifier</label>
<type>text</type>
<help>(optional) This can be either an IP address, fully qualified domain name or an email address to identify the remote host.</help>
</field>
<field>
<id>preSharedKey.Key</id>
<label>Pre-Shared Key</label>

View File

@ -0,0 +1,22 @@
<form>
<field>
<id>pool.enabled</id>
<label>enabled</label>
<type>checkbox</type>
</field>
<field>
<id>pool.name</id>
<label>Name</label>
<type>text</type>
</field>
<field>
<id>pool.addrs</id>
<label>Network</label>
<type>text</type>
<help>
Subnet or range defining addresses allocated in pool.
Accepts a single CIDR subnet defining the pool to allocate addresses from
</help>
</field>
</form>

View File

@ -0,0 +1,62 @@
<form>
<field>
<id>remote.enabled</id>
<label>enabled</label>
<type>checkbox</type>
</field>
<field>
<id>remote.connection</id>
<label>Connection</label>
<type>dropdown</type>
</field>
<field>
<id>remote.round</id>
<label>Round</label>
<type>text</type>
<help>Numeric identifier by which authentication rounds are sorted.</help>
</field>
<field>
<id>remote.auth</id>
<label>Authentication</label>
<type>dropdown</type>
<help>Authentication to perform for this round</help>
</field>
<field>
<id>remote.id</id>
<label>Id</label>
<type>text</type>
<help>IKE identity to use for authentication round.
When using certificate authentication.
The IKE identity must be contained in the certificate,
either as the subject DN or as a subjectAltName
(the identity will default to the certificates subject DN if not specified).
Refer to https://docs.strongswan.org/docs/5.9/config/identityParsing.html for details on how
identities are parsed and may be configured.
</help>
</field>
<field>
<id>remote.eap_id</id>
<label>EAP Id</label>
<type>text</type>
<help>Client EAP-Identity to use in EAP-Identity exchange and the EAP method</help>
<style>remote_auth remote_auth_eap-mschapv2 remote_auth_eap-tls remote_auth_eap-radius</style>
</field>
<field>
<id>remote.certs</id>
<label>Certificates</label>
<type>select_multiple</type>
<help>List of certificate candidates to use for authentication.</help>
</field>
<field>
<id>remote.pubkeys</id>
<label>Public Keys</label>
<type>select_multiple</type>
<help>List of raw public key candidates to use for authentication.</help>
<style>remote_auth remote_auth_pubkey</style>
</field>
<field>
<id>remote.description</id>
<label>Description</label>
<type>text</type>
</field>
</form>

View File

@ -0,0 +1,47 @@
<form>
<field>
<id>vti.enabled</id>
<label>enabled</label>
<type>checkbox</type>
</field>
<field>
<id>vti.reqid</id>
<label>Reqid</label>
<type>text</type>
<help>
This id is used to distinguish traffic and security policies between several if_ipsec interfaces.
</help>
</field>
<field>
<id>vti.local</id>
<label>Local address</label>
<type>text</type>
<help>Local tunnel address used for the outer IP header of ESP packets</help>
</field>
<field>
<id>vti.remote</id>
<label>Remote address</label>
<type>text</type>
<help>Remote tunnel address used for the outer IP header of ESP packets</help>
</field>
<field>
<id>vti.tunnel_local</id>
<label>Tunnel local address</label>
<type>text</type>
<help>Inner tunnel local address to be used for routing purposes.</help>
</field>
<field>
<id>vti.tunnel_remote</id>
<label>Tunnel remote address</label>
<type>text</type>
<help>
Inner tunnel remote address to be used for routing purposes.
The size of the subnet containing local and remote will be calculated automatically
</help>
</field>
<field>
<id>vti.description</id>
<label>Name</label>
<type>text</type>
</field>
</form>

View File

@ -17,6 +17,17 @@
<pattern>api/ipsec/legacy-subsystem/*</pattern>
</patterns>
</page-vpn-ipsec>
<page-vpn-ipsec-connections>
<name>VPN: IPsec connections [new]</name>
<patterns>
<pattern>ui/ipsec/connections</pattern>
<pattern>ui/ipsec/vti</pattern>
<pattern>api/ipsec/connections/*</pattern>
<pattern>api/ipsec/pools/*</pattern>
<pattern>api/ipsec/vti/*</pattern>
<pattern>api/ipsec/legacy-subsystem/*</pattern>
</patterns>
</page-vpn-ipsec-connections>
<!-- ACLs for legacy code -->
<page-vpn-ipsec-editphase1>

View File

@ -0,0 +1,81 @@
<?php
/**
* Copyright (C) 2022 Deciso B.V.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/
namespace OPNsense\IPsec\FieldTypes;
use OPNsense\Base\FieldTypes\ArrayField;
use OPNsense\Base\FieldTypes\TextField;
class ConnnectionField extends ArrayField
{
private static $child_attrs = ['local_ts', 'remote_ts'];
private static $child_data = null;
/**
* Add child attributes (virtual / read-only) to connection for query purposes
*/
protected function actionPostLoadingEvent()
{
if (self::$child_data === null) {
self::$child_data = [];
foreach ($this->getParentModel()->children->child->iterateItems() as $node_uuid => $node) {
if (empty((string)$node->enabled)) {
continue;
}
$conn_uuid = (string)$node->connection;
if (!isset(self::$child_data[$conn_uuid])) {
self::$child_data[$conn_uuid] = [];
}
foreach (self::$child_attrs as $key) {
if (!isset(self::$child_data[$conn_uuid][$key])) {
self::$child_data[$conn_uuid][$key] = [];
}
self::$child_data[$conn_uuid][$key][] = (string)$node->$key;
}
}
}
foreach ($this->internalChildnodes as $node) {
if (!$node->getInternalIsVirtual()) {
$extra_attr = ['local_ts' => '', 'remote_ts' => ''];
$conn_uuid = (string)$node->getAttribute('uuid');
foreach (self::$child_attrs as $key) {
$child_node = new TextField();
$child_node->setInternalIsVirtual();
if (isset(self::$child_data[$conn_uuid]) && !empty(self::$child_data[$conn_uuid][$key])) {
$child_node->setValue(implode(',', array_unique(self::$child_data[$conn_uuid][$key])));
}
$node->addChildNode($key, $child_node);
}
}
}
return parent::actionPostLoadingEvent();
}
}

View File

@ -0,0 +1,80 @@
<?php
/*
* Copyright (C) 2022 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\IPsec\FieldTypes;
use OPNsense\Base\FieldTypes\BaseField;
use OPNsense\Base\Validators\CallbackValidator;
use OPNsense\Firewall\Util;
/**
* @package OPNsense\Base\FieldTypes
*/
class IKEAdressField extends BaseField
{
/**
* @var bool marks if this is a data node or a container
*/
protected $internalIsContainer = false;
/**
* get valid options, descriptions and selected value
* @return array
*/
public function getNodeData()
{
$result = [];
foreach (explode(',', $this->internalValue) as $net) {
$result[$net] = array("value" => $net, "selected" => 1);
}
return $result;
}
public function getValidators()
{
$validators = parent::getValidators();
if ($this->internalValue != null) {
$validators[] = new CallbackValidator(["callback" => function ($data) {
$messages = [];
foreach (explode(",", $data) as $entry) {
if (Util::isIpAddress($entry) || Util::isSubnet($entry) || Util::isDomain($entry)) {
continue;
}
$messages[] = sprintf(
gettext('Entry "%s" is not a valid hostname, IP address or range.'),
$entry
);
}
return $messages;
}
]);
}
return $validators;
}
}

View File

@ -0,0 +1,62 @@
<?php
/*
* Copyright (C) 2022 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\IPsec\FieldTypes;
use OPNsense\Base\FieldTypes\BaseListField;
/**
* @package OPNsense\Base\FieldTypes
*/
class IPsecProposalField extends BaseListField
{
private static $internalCacheOptionList = [];
protected function actionPostLoadingEvent()
{
if (empty(self::$internalCacheOptionList)) {
self::$internalCacheOptionList['default'] = 'default';
foreach (['aes128', 'aes192', 'aes256', 'aes128gcm16', 'aes192gcm16', 'aes256gcm16',
'chacha20poly1305'] as $encalg
) {
foreach (['sha256', 'sha384', 'sha512', 'aesxcbc'] as $intalg) {
foreach ([
'modp2048', 'modp3072', 'modp4096', 'modp6144', 'modp8192', 'ecp224',
'ecp256', 'ecp384', 'ecp521', 'ecp224bp', 'ecp256bp', 'ecp384bp', 'ecp512bp',
'x25519', 'x448'] as $dhgroup
) {
$cipher = "{$encalg}-{$intalg}-{$dhgroup}";
self::$internalCacheOptionList[$cipher] = $cipher;
}
}
}
natcasesort(self::$internalCacheOptionList);
}
$this->internalOptionList = self::$internalCacheOptionList;
}
}

View File

@ -0,0 +1,52 @@
<?php
/*
* Copyright (C) 2022 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\IPsec\FieldTypes;
use OPNsense\Base\FieldTypes\BaseListField;
/**
* @package OPNsense\Base\FieldTypes
*/
class PoolsField extends BaseListField
{
private static $internalCacheOptionList = [];
protected function actionPostLoadingEvent()
{
if (empty(self::$internalCacheOptionList)) {
foreach ($this->getParentModel()->Pools->Pool->iterateItems() as $node_uuid => $node) {
self::$internalCacheOptionList[$node_uuid] = (string)$node->name;
}
// internal (plugin) pools
self::$internalCacheOptionList['radius'] = 'radius';
natcasesort(self::$internalCacheOptionList);
}
$this->internalOptionList = self::$internalCacheOptionList;
}
}

View File

@ -0,0 +1,134 @@
<?php
/**
* Copyright (C) 2022 Deciso B.V.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/
namespace OPNsense\IPsec\FieldTypes;
use OPNsense\Base\FieldTypes\ArrayField;
use OPNsense\Base\FieldTypes\TextField;
use OPNsense\Core\Backend;
class VTIField extends ArrayField
{
private static $legacyItems = [];
public function __construct($ref = null, $tagname = null)
{
if (empty(self::$legacyItems)) {
// query legacy VTI devices, valid for the duration of this script execution
$legacy_vtis = json_decode((new Backend())->configdRun('ipsec list legacy_vti'), true);
if (!empty($legacy_vtis)) {
foreach ($legacy_vtis as $vti) {
$vti['enabled'] = '1';
self::$legacyItems['ipsec'.$vti['reqid']] = $vti;
}
}
}
parent::__construct($ref, $tagname);
}
/**
* create virtual VTI nodes
*/
private function createReservedNodes()
{
$result = [];
foreach (self::$legacyItems as $vtiName => $vtiContent) {
$container_node = $this->newContainerField($this->__reference . "." . $vtiName, $this->internalXMLTagName);
$container_node->setAttributeValue("uuid", $vtiName);
$container_node->setInternalIsVirtual();
foreach ($this->getTemplateNode()->iterateItems() as $key => $value) {
$node = clone $value;
$node->setInternalReference($container_node->__reference . "." . $key);
if (isset($vtiContent[$key])) {
$node->setValue($vtiContent[$key]);
}
$node->markUnchanged();
$container_node->addChildNode($key, $node);
}
$type_node = new TextField();
$type_node->setInternalIsVirtual();
$type_node->setValue('legacy');
$container_node->addChildNode('origin', $type_node);
$result[$vtiName] = $container_node;
}
return $result;
}
protected function actionPostLoadingEvent()
{
foreach ($this->internalChildnodes as $node) {
if (!$node->getInternalIsVirtual()) {
$type_node = new TextField();
$type_node->setInternalIsVirtual();
$type_node->setValue('vti');
$node->addChildNode('origin', $type_node);
}
}
return parent::actionPostLoadingEvent();
}
/**
* @inheritdoc
*/
public function hasChild($name)
{
if (isset(self::$reservedItems[$name])) {
return true;
} else {
return parent::hasChild($name);
}
}
/**
* @inheritdoc
*/
public function getChild($name)
{
if (isset(self::$reservedItems[$name])) {
return $this->createReservedNodes()[$name];
} else {
return parent::getChild($name);
}
}
/**
* @inheritdoc
*/
public function iterateItems()
{
foreach (parent::iterateItems() as $key => $value) {
yield $key => $value;
}
foreach ($this->createReservedNodes() as $key => $node) {
yield $key => $node;
}
}
}

View File

@ -43,9 +43,18 @@
<check001>
<ValidationMessage>Another entry with the same identifier already exists.</ValidationMessage>
<type>UniqueConstraint</type>
<addField>remote_ident</addField>
</check001>
</Constraints>
</ident>
<remote_ident type="TextField">
<Required>N</Required>
<mask>/^([a-zA-Z0-9@\.\-]*)/u</mask>
<ValidationMessage>The identifier contains invalid characters.</ValidationMessage>
<Constraints>
<reference>ident.check001</reference>
</Constraints>
</remote_ident>
<keyType type="OptionField">
<Required>Y</Required>
<default>PSK</default>

View File

@ -1,6 +1,7 @@
<menu>
<VPN>
<IPsec cssClass="fa fa-lock fa-fw" order="10">
<Connections order="5" VisibleName="Connections [new]" url="/ui/ipsec/connections"/>
<Tunnels order="10" VisibleName="Tunnel Settings" url="/ui/ipsec/tunnels">
<Phase1 url="/vpn_ipsec_phase1.php*" visibility="hidden"/>
<Phase2 url="/vpn_ipsec_phase2.php*" visibility="hidden"/>
@ -15,6 +16,7 @@
<Leases order="70" VisibleName="Lease Status" url="/ui/ipsec/leases"/>
<SAD order="80" VisibleName="Security Association Database" url="/ui/ipsec/sad"/>
<SPD order="90" VisibleName="Security Policy Database" url="/ui/ipsec/spd"/>
<VTI order="90" VisibleName="Virtual Tunnel Interfaces" url="/ui/ipsec/vti"/>
<LogFile order="100" VisibleName="Log File" url="/ui/diagnostics/log/core/ipsec"/>
</IPsec>
</VPN>

View File

@ -0,0 +1,191 @@
<?php
/*
* Copyright (C) 2022 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\IPsec;
use Phalcon\Messages\Message;
use OPNsense\Base\BaseModel;
use OPNsense\Firewall\Util;
/**
* Class Swanctl
* @package OPNsense\IPsec
*/
class Swanctl extends BaseModel
{
/**
* {@inheritdoc}
*/
public function performValidation($validateFullModel = false)
{
$messages = parent::performValidation($validateFullModel);
$vtis = [];
foreach ($this->getFlatNodes() as $key => $node) {
if ($validateFullModel || $node->isFieldChanged()) {
$tagName = $node->getInternalXMLTagName();
$parentNode = $node->getParentNode();
$parentKey = $parentNode->__reference;
$parentTagName = $parentNode->getInternalXMLTagName();
if ($parentTagName === 'VTI') {
$vtis[$parentKey] = $parentNode;
}
}
}
foreach ($vtis as $key => $node) {
$vti_inets = [];
foreach (['local', 'remote', 'tunnel_local', 'tunnel_remote'] as $prop) {
$vti_inets[$prop] = strpos((string)$node->$prop, ':') > 0 ? 'inet6' : 'inet';
}
if ($vti_inets['local'] != $vti_inets['remote']) {
$messages->appendMessage(new Message(gettext("Protocol families should match"), $key . ".local"));
}
if ($vti_inets['tunnel_local'] != $vti_inets['tunnel_remote']) {
$messages->appendMessage(new Message(gettext("Protocol families should match"), $key . ".tunnel_local"));
}
}
return $messages;
}
/**
* generate swanctl configuration output, containing "pools" and "connections", locals, remotes and children
* are treated as children of connection.
* @return array
*/
public function getConfig()
{
$data = ['connections' => [], 'pools' => []];
$references = [
'pools' => 'Pools.Pool',
'connections' => 'Connections.Connection',
'locals' => 'locals.local',
'remotes' => 'remotes.remote',
'children' => 'children.child',
];
foreach ($references as $key => $ref) {
foreach ($this->getNodeByReference($ref)->iterateItems() as $node_uuid => $node) {
if (empty((string)$node->enabled)) {
continue;
}
$parent = null;
$thisnode = [];
foreach ($node->iterateItems() as $attr_name => $attr) {
if ($attr_name == 'connection' && isset($data['connections'][(string)$attr])) {
$parent = (string)$attr;
continue;
} elseif ($attr_name == 'pools') {
// pools are mapped by name for clearer identification and legacy support
if ((string)$attr != '') {
$pools = [];
foreach (explode(',', (string)$attr) as $pool_id) {
$is_uuid = preg_match(
'/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $pool_id
) == 1;
if (isset($data['pools'][$pool_id])) {
$pools[] = $data['pools'][$pool_id]['name'];
} elseif (!$is_uuid) {
$pools[] = $pool_id;
}
}
if (!empty($pools)) {
$thisnode['pools'] = implode(',', $pools);
}
}
continue;
} elseif ($attr_name == 'enabled') {
if (empty((string)$attr)) {
// disabled entity
$thisnode = [];
break;
} else {
continue;
}
} elseif ((string)$attr == '') {
continue;
} elseif (is_a($attr, 'OPNsense\Base\FieldTypes\BooleanField')) {
$thisnode[$attr_name] = (string)$attr == '1' ? 'yes' : 'no';
} elseif ($attr_name == 'pubkeys') {
$tmp = [];
foreach (explode(',', (string)$attr) as $item) {
$tmp[] = $item . '.pem';
}
$thisnode[$attr_name] = implode(',', $tmp);
} else {
$thisnode[$attr_name] = (string)$attr;
}
}
if (empty($thisnode)) {
continue;
} elseif (!empty($parent)) {
if (!isset($data['connections'][$parent][$key])) {
$data['connections'][$parent][$key] = [];
}
$data['connections'][$parent][$key][] = $thisnode;
} else {
if (!isset($data[$key])) {
$data[$key] = [];
}
$data[$key][$node_uuid] = $thisnode;
}
}
}
return $data;
}
/**
* return non legacy vti devices formatted like ipsec_get_configured_vtis()
*/
public function getVtiDevices()
{
$result = [];
foreach ($this->VTIs->VTI->iterateItems() as $node_uuid => $node) {
if ((string)$node->origin != 'legacy' && (string)$node->enabled == '1') {
$inet = strpos((string)$node->local_tunnel, ':') > 0 ? 'inet6' : 'inet';
$result['ipsec' . (string)$node->reqid] = [
'reqid' => (string)$node->reqid,
'local' => (string)$node->local,
'remote' => (string)$node->remote,
'descr' => (string)$node->description,
'networks' => [
[
'inet' => $inet,
'tunnel_local' => (string)$node->tunnel_local,
'tunnel_remote' => (string)$node->tunnel_remote,
'mask' => Util::smallestCIDR(
[(string)$node->tunnel_local, (string)$node->tunnel_remote],
$inet
)
]
]
];
}
}
return $result;
}
}

View File

@ -0,0 +1,392 @@
<model>
<mount>//OPNsense/Swanctl</mount>
<version>1.0.0</version>
<description>OPNsense IPsec Connections</description>
<items>
<Connections>
<Connection type=".\ConnnectionField">
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<proposals type=".\IPsecProposalField">
<default>default</default>
<Required>Y</Required>
</proposals>
<unique type="OptionField">
<Required>Y</Required>
<default>no</default>
<OptionValues>
<no>No (default)</no>
<never>Never</never>
<keep>Keep</keep>
<replace>Replace</replace>
</OptionValues>
</unique>
<aggressive type="BooleanField">
<default>0</default>
<Required>Y</Required>
</aggressive>
<version type="OptionField">
<Required>Y</Required>
<default>0</default>
<OptionValues>
<ike value="0">IKEv1+IKEv2</ike>
<ikev1 value="1">IKEv1</ikev1>
<ikev2 value="2">IKEv2</ikev2>
</OptionValues>
</version>
<mobike type="BooleanField">
<default>1</default>
<Required>Y</Required>
</mobike>
<local_addrs type=".\IKEAdressField">
<Required>N</Required>
</local_addrs>
<remote_addrs type=".\IKEAdressField">
<Required>N</Required>
</remote_addrs>
<encap type="BooleanField">
<default>0</default>
<Required>Y</Required>
</encap>
<reauth_time type="IntegerField">
<MinimumValue>0</MinimumValue>
<MaximumValue>500000</MaximumValue>
<Required>N</Required>
</reauth_time>
<rekey_time type="IntegerField">
<MinimumValue>0</MinimumValue>
<MaximumValue>500000</MaximumValue>
<Required>N</Required>
</rekey_time>
<over_time type="IntegerField">
<MinimumValue>0</MinimumValue>
<MaximumValue>500000</MaximumValue>
<Required>N</Required>
</over_time>
<dpd_delay type="IntegerField">
<MinimumValue>0</MinimumValue>
<MaximumValue>500000</MaximumValue>
<Required>N</Required>
</dpd_delay>
<dpd_timeout type="IntegerField">
<MinimumValue>0</MinimumValue>
<MaximumValue>500000</MaximumValue>
<Required>N</Required>
</dpd_timeout>
<pools type=".\PoolsField">
<Required>N</Required>
<Multiple>Y</Multiple>
</pools>
<send_certreq type="BooleanField">
<default>1</default>
<Required>Y</Required>
</send_certreq>
<send_cert type="OptionField">
<Required>N</Required>
<BlankDesc>Default</BlankDesc>
<OptionValues>
<ifasked>If asked</ifasked>
<never>Never</never>
<always>Always</always>
</OptionValues>
</send_cert>
<keyingtries type="IntegerField">
<MinimumValue>0</MinimumValue>
<MaximumValue>1000</MaximumValue>
<Required>N</Required>
</keyingtries>
<description type="TextField">
<Required>N</Required>
</description>
</Connection>
</Connections>
<locals>
<local type="ArrayField">
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<connection type="ModelRelationField">
<Model>
<host>
<source>OPNsense.IPsec.Swanctl</source>
<items>Connections.Connection</items>
<display>description</display>
</host>
</Model>
<Required>Y</Required>
</connection>
<round type="IntegerField">
<Required>Y</Required>
<MinimumValue>0</MinimumValue>
<MaximumValue>10</MaximumValue>
<default>0</default>
</round>
<auth type="OptionField">
<Required>Y</Required>
<default>psk</default>
<OptionValues>
<psk>Pre-Shared Key</psk>
<pubkey>Public Key</pubkey>
<eap_tls value="eap-tls">EAP TLS</eap_tls>
<eap_mschapv2 value="eap-mschapv2">EAP-MSCHAPv2</eap_mschapv2>
<xauth_pam value="xauth-pam">Xauth PAM</xauth_pam>
<eap_radius value="eap-radius">EAP RADIUS</eap_radius>
</OptionValues>
</auth>
<id type="TextField">
<Required>N</Required>
<mask>/^([0-9a-zA-Z.\-,_\:){0,4196}$/u</mask>
</id>
<eap_id type="TextField">
<Required>N</Required>
<mask>/^([0-9a-zA-Z.\-,_\:){0,4196}$/u</mask>
</eap_id>
<certs type="CertificateField">
<Required>N</Required>
<Multiple>Y</Multiple>
<ValidationMessage>Please select a valid certificate from the list</ValidationMessage>
</certs>
<pubkeys type="ModelRelationField">
<Model>
<host>
<source>OPNsense.IPsec.IPsec</source>
<items>keyPairs.keyPair</items>
<display>name</display>
</host>
</Model>
<Multiple>Y</Multiple>
<Required>N</Required>
</pubkeys>
<description type="TextField">
<Required>N</Required>
</description>
</local>
</locals>
<remotes>
<remote type="ArrayField">
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<connection type="ModelRelationField">
<Model>
<host>
<source>OPNsense.IPsec.Swanctl</source>
<items>Connections.Connection</items>
<display>description</display>
</host>
</Model>
<Required>Y</Required>
</connection>
<round type="IntegerField">
<Required>Y</Required>
<MinimumValue>0</MinimumValue>
<MaximumValue>10</MaximumValue>
<default>0</default>
</round>
<auth type="OptionField">
<Required>Y</Required>
<default>psk</default>
<OptionValues>
<psk>Pre-Shared Key</psk>
<pubkey>Public Key</pubkey>
<eap_tls value="eap-tls">EAP TLS</eap_tls>
<eap_mschapv2 value="eap-mschapv2">EAP-MSCHAPv2</eap_mschapv2>
<xauth_pam value="xauth-pam">Xauth PAM</xauth_pam>
<eap_radius value="eap-radius">EAP RADIUS</eap_radius>
</OptionValues>
</auth>
<id type="TextField">
<Required>N</Required>
<mask>/^([0-9a-zA-Z.\-,_\:){0,4196}$/u</mask>
</id>
<eap_id type="TextField">
<Required>N</Required>
<mask>/^([0-9a-zA-Z.\-,_\:){0,4196}$/u</mask>
</eap_id>
<certs type="CertificateField">
<Required>N</Required>
<Multiple>Y</Multiple>
<ValidationMessage>Please select a valid certificate from the list</ValidationMessage>
</certs>
<pubkeys type="ModelRelationField">
<Model>
<host>
<source>OPNsense.IPsec.IPsec</source>
<items>keyPairs.keyPair</items>
<display>name</display>
</host>
</Model>
<Multiple>Y</Multiple>
<Required>N</Required>
</pubkeys>
<description type="TextField">
<Required>N</Required>
</description>
</remote>
</remotes>
<children>
<child type="ArrayField">
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<connection type="ModelRelationField">
<Model>
<host>
<source>OPNsense.IPsec.Swanctl</source>
<items>Connections.Connection</items>
<display>description</display>
</host>
</Model>
<Required>Y</Required>
</connection>
<reqid type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>65535</MaximumValue>
<Required>N</Required>
</reqid>
<esp_proposals type=".\IPsecProposalField">
<default>default</default>
<Required>Y</Required>
</esp_proposals>
<sha256_96 type="BooleanField">
<default>0</default>
<Required>Y</Required>
</sha256_96>
<start_action type="OptionField">
<Required>Y</Required>
<default>start</default>
<OptionValues>
<none>None</none>
<trap_start value='trap|start'>Trap+start</trap_start>
<route>Route</route>
<start>Start</start>
<trap>Trap</trap>
</OptionValues>
</start_action>
<close_action type="OptionField">
<Required>Y</Required>
<default>none</default>
<OptionValues>
<none>None</none>
<trap>Trap</trap>
<start>Start</start>
</OptionValues>
</close_action>
<dpd_action type="OptionField">
<Required>Y</Required>
<default>clear</default>
<OptionValues>
<clear>Clear</clear>
<trap>Trap</trap>
<start>Start</start>
</OptionValues>
</dpd_action>
<mode type="OptionField">
<Required>Y</Required>
<default>tunnel</default>
<OptionValues>
<tunnel>Tunnel</tunnel>
<transport>Transport</transport>
<pass>Pass</pass>
<drop>Drop</drop>
</OptionValues>
</mode>
<local_ts type="NetworkField">
<Required>Y</Required>
<FieldSeparator>,</FieldSeparator>
<asList>Y</asList>
<WildcardEnabled>N</WildcardEnabled>
</local_ts>
<remote_ts type="NetworkField">
<Required>Y</Required>
<FieldSeparator>,</FieldSeparator>
<asList>Y</asList>
<WildcardEnabled>N</WildcardEnabled>
</remote_ts>
<rekey_time type="IntegerField">
<default>3600</default>
<MinimumValue>0</MinimumValue>
<MaximumValue>500000</MaximumValue>
<Required>Y</Required>
</rekey_time>
<description type="TextField">
<Required>N</Required>
</description>
</child>
</children>
<Pools>
<Pool type="ArrayField">
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<name type="TextField">
<Required>N</Required>
<Constraints>
<check001>
<ValidationMessage>Pool name must be unique.</ValidationMessage>
<type>UniqueConstraint</type>
</check001>
</Constraints>
</name>
<addrs type="NetworkField">
<Required>Y</Required>
<WildcardEnabled>N</WildcardEnabled>
<NetMaskRequired>Y</NetMaskRequired>
<ValidationMessage>Please specify a valid CIDR subnet.</ValidationMessage>
</addrs>
</Pool>
</Pools>
<VTIs>
<VTI type=".\VTIField">
<enabled type="BooleanField">
<default>1</default>
<Required>Y</Required>
</enabled>
<reqid type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>65535</MaximumValue>
<Required>Y</Required>
<Constraints>
<check001>
<ValidationMessage>Reqid must be unique.</ValidationMessage>
<type>UniqueConstraint</type>
</check001>
</Constraints>
</reqid>
<local type="NetworkField">
<NetMaskAllowed>N</NetMaskAllowed>
<WildcardEnabled>N</WildcardEnabled>
<Required>Y</Required>
<ValidationMessage>Please specify a valid address.</ValidationMessage>
</local>
<remote type="NetworkField">
<NetMaskAllowed>N</NetMaskAllowed>
<WildcardEnabled>N</WildcardEnabled>
<Required>Y</Required>
<ValidationMessage>Please specify a valid address.</ValidationMessage>
</remote>
<tunnel_local type="NetworkField">
<NetMaskAllowed>N</NetMaskAllowed>
<WildcardEnabled>N</WildcardEnabled>
<Required>Y</Required>
<ValidationMessage>Please specify a valid address.</ValidationMessage>
</tunnel_local>
<tunnel_remote type="NetworkField">
<NetMaskAllowed>N</NetMaskAllowed>
<WildcardEnabled>N</WildcardEnabled>
<Required>Y</Required>
<ValidationMessage>Please specify a valid address.</ValidationMessage>
</tunnel_remote>
<description type="TextField">
<Required>N</Required>
</description>
</VTI>
</VTIs>
</items>
</model>

View File

@ -0,0 +1,311 @@
<script>
$( document ).ready(function() {
let grid_connections = $("#grid-connections").UIBootgrid({
search:'/api/ipsec/connections/search_connection',
get:'/api/ipsec/connections/get_connection/',
set:'/api/ipsec/connections/set_connection/',
add:'/api/ipsec/connections/set_connection/',
del:'/api/ipsec/connections/del_connection/',
toggle:'/api/ipsec/connections/toggle_connection/',
});
let grid_pools = $("#grid-pools").UIBootgrid({
search:'/api/ipsec/pools/search',
get:'/api/ipsec/pools/get/',
set:'/api/ipsec/pools/set/',
add:'/api/ipsec/pools/add/',
del:'/api/ipsec/pools/del/',
toggle:'/api/ipsec/pools/toggle/',
});
let detail_grids = {
locals: 'local',
remotes: 'remote',
children: 'child',
};
for (const [grid_key, obj_type] of Object.entries(detail_grids)) {
$("#grid-" + grid_key).UIBootgrid({
search:'/api/ipsec/connections/search_' + obj_type,
get:'/api/ipsec/connections/get_' + obj_type + '/',
set:'/api/ipsec/connections/set_' + obj_type + '/',
add:'/api/ipsec/connections/add_' + obj_type + '/',
del:'/api/ipsec/connections/del_' + obj_type + '/',
toggle:'/api/ipsec/connections/toggle_' + obj_type + '/',
options:{
navigation: obj_type === 'child' ? 3 : 0,
selection: obj_type === 'child' ? true : false,
useRequestHandlerOnGet: true,
requestHandler: function(request) {
request['connection'] = $("#connection\\.uuid").val();
if (request.rowCount === undefined) {
// XXX: We can't easily see if we're being called by GET or POST, buf if no rowCount is being offered
// it's highly likely a POST from bootgrid
return new URLSearchParams(request).toString();
} else {
return request
}
}
}
});
if (obj_type !== 'child') {
$("#"+obj_type+"\\.auth").change(function(){
$("."+obj_type+"_auth").closest("tr").hide();
$("."+obj_type+"_auth_"+$(this).val()).each(function(){
$(this).closest("tr").show();
});
});
}
}
$(".hidden_attr").closest('tr').hide();
$("#ConnectionDialog").click(function(){
$("#connection_details").hide();
ajaxGet("/api/ipsec/connections/connection_exists/" + $("#connection\\.uuid").val(), {}, function(data){
if (data.exists) {
$("#connection_details").show();
$("#grid-locals").bootgrid("reload");
$("#grid-remotes").bootgrid("reload");
$("#grid-children").bootgrid("reload");
}
});
$(this).show();
});
$("#ConnectionDialog").change(function(){
if ($("#connection_details").is(':visible')) {
$("#tab_connections").click();
$("#ConnectionDialog").hide();
} else {
$("#ConnectionDialog").click();
}
});
$("#connection\\.description").change(function(){
if ($(this).val() !== '') {
$("#ConnectionDialog").text($(this).val());
} else {
$("#ConnectionDialog").text('-');
}
});
$("#frm_ConnectionDialog").append($("#frm_DialogConnection").detach());
updateServiceControlUI('ipsec');
/**
* reconfigure
*/
$("#reconfigureAct").SimpleActionButton();
});
</script>
<style>
div.section_header > hr {
margin: 0px;
}
div.section_header > h2 {
padding-left: 5px;
margin: 0px;
}
</style>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" id="tab_connections" href="#connections">{{ lang._('Connections') }}</a></li>
<li><a data-toggle="tab" href="#edit_connection" id="ConnectionDialog" style="display: none;"> </a></li>
<li><a data-toggle="tab" href="#pools" id="tab_pools"> {{ lang._('Pools') }} </a></li>
</ul>
<div class="tab-content content-box">
<div id="connections" class="tab-pane fade in active">
<table id="grid-connections" class="table table-condensed table-hover table-striped" data-editDialog="ConnectionDialog" data-editAlert="ConnectionChangeMessage">
<thead>
<tr>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="local_addrs" data-type="string">{{ lang._('Local') }}</th>
<th data-column-id="remote_addrs" data-type="string">{{ lang._('Remote') }}</th>
<th data-column-id="local_ts" data-type="string">{{ lang._('Local Nets') }}</th>
<th data-column-id="remote_ts" data-type="string">{{ lang._('Remote Nets') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-primary"><span class="fa fa-fw fa-plus"></span></button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-fw fa-trash-o"></span></button>
</td>
</tr>
</tfoot>
</table>
<div class="col-md-12">
<div id="ConnectionChangeMessage" class="alert alert-info" style="display: none" role="alert">
{{ lang._('After changing settings, please remember to apply them with the button below') }}
</div>
<hr/>
</div>
<div class="col-md-12">
<button class="btn btn-primary" id="reconfigureAct"
data-endpoint='/api/ipsec/legacy-subsystem/applyConfig'
data-label="{{ lang._('Apply') }}"
data-error-title="{{ lang._('Error reconfiguring IPsec') }}"
type="button"
></button>
<br/><br/>
</div>
</div>
<div id="edit_connection" class="tab-pane fade in">
<div class="section_header">
<h2>{{ lang._('General settings')}}</h2>
<hr/>
</div>
<div>
<form id="frm_ConnectionDialog">
</form>
</div>
<div id="connection_details">
<div class="row">
<hr/>
<div class="col-xs-6">
<div class="section_header">
<h2>{{ lang._('Local Authentication')}}</h2>
<hr/>
</div>
<table id="grid-locals" class="table table-condensed table-hover table-striped" data-editDialog="DialogLocal">
<thead>
<tr>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="round" data-type="string">{{ lang._('Round') }}</th>
<th data-column-id="auth" data-type="string">{{ lang._('Authentication') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-primary pull-right"><span class="fa fa-fw fa-plus"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
<div class="col-xs-6">
<div class="section_header">
<h2>{{ lang._('Remote Authentication')}}</h2>
<hr/>
</div>
<table id="grid-remotes" class="table table-condensed table-hover table-striped" data-editDialog="DialogRemote">
<thead>
<tr>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="round" data-type="string">{{ lang._('Round') }}</th>
<th data-column-id="auth" data-type="string">{{ lang._('Authentication') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-primary pull-right"><span class="fa fa-fw fa-plus"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="section_header">
<h2>{{ lang._('Children')}}</h2>
<hr/>
</div>
<table id="grid-children" class="table table-condensed table-hover table-striped" data-editDialog="DialogChild">
<thead>
<tr>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="local_ts" data-type="string">{{ lang._('Local Nets') }}</th>
<th data-column-id="remote_ts" data-type="string">{{ lang._('Remote Nets') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-primary"><span class="fa fa-fw fa-plus"></span></button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-fw fa-trash-o"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="col-md-12">
<div id="ConnectionDialogBtns">
<button type="button" class="btn btn-primary" id="btn_ConnectionDialog_save">
<strong>{{ lang._('Save')}}</strong>
<i id="btn_ConnectionDialog_save_progress" class=""></i>
</button>
</div>
<br/>
</div>
</div>
<div id="pools" class="tab-pane fade in">
<table id="grid-pools" class="table table-condensed table-hover table-striped" data-editDialog="DialogPool" data-editAlert="PoolChangeMessage">
<thead>
<tr>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="name" data-type="string">{{ lang._('Name') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-primary"><span class="fa fa-fw fa-plus"></span></button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-fw fa-trash-o"></span></button>
</td>
</tr>
</tfoot>
</table>
<div class="col-md-12">
<div id="PoolChangeMessage" class="alert alert-info" style="display: none" role="alert">
{{ lang._('After changing settings, please remember to apply them') }}
</div>
<hr/>
</div>
</div>
</div>
{{ partial("layout_partials/base_dialog",['fields':formDialogConnection,'id':'DialogConnection','label':lang._('Edit Connection')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogLocal,'id':'DialogLocal','label':lang._('Edit Local')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogRemote,'id':'DialogRemote','label':lang._('Edit Remote')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogChild,'id':'DialogChild','label':lang._('Edit Child')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogPool,'id':'DialogPool','label':lang._('Edit Pool')])}}

View File

@ -16,7 +16,8 @@
<thead>
<tr>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="ident" data-type="string">{{ lang._('Identifier') }}</th>
<th data-column-id="ident" data-type="string">{{ lang._('Local Identifier') }}</th>
<th data-column-id="remote_ident" data-type="string">{{ lang._('Remote Identifier') }}</th>
<th data-column-id="keyType" data-width="20em" data-type="string">{{ lang._('Key Type') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>

View File

@ -0,0 +1,94 @@
<script>
$( document ).ready(function() {
let grid_vti = $("#grid-vti").UIBootgrid({
search:'/api/ipsec/vti/search',
get:'/api/ipsec/vti/get/',
set:'/api/ipsec/vti/set/',
add:'/api/ipsec/vti/add/',
del:'/api/ipsec/vti/del/',
toggle:'/api/ipsec/vti/toggle/',
options:{
formatters: {
commands: function (column, row) {
if (row.uuid.includes('-') === true) {
// exclude buttons for internal aliases (which uses names instead of valid uuid's)
return '<button type="button" class="btn btn-xs btn-default command-edit bootgrid-tooltip" data-row-id="' + row.uuid + '"><span class="fa fa-fw fa-pencil"></span></button> ' +
'<button type="button" class="btn btn-xs btn-default command-copy bootgrid-tooltip" data-row-id="' + row.uuid + '"><span class="fa fa-fw fa-clone"></span></button>' +
'<button type="button" class="btn btn-xs btn-default command-delete bootgrid-tooltip" data-row-id="' + row.uuid + '"><span class="fa fa-fw fa-trash-o"></span></button>';
}
},
tunnel: function (column, row) {
return row.tunnel_local + ' <-> ' + row.tunnel_remote;
}
}
}
});
updateServiceControlUI('ipsec');
/**
* reconfigure
*/
$("#reconfigureAct").SimpleActionButton();
});
</script>
<style>
div.section_header > hr {
margin: 0px;
}
div.section_header > h2 {
padding-left: 5px;
margin: 0px;
}
</style>
<div class="content-box">
<table id="grid-vti" class="table table-condensed table-hover table-striped" data-editDialog="DialogVTI" data-editAlert="VTIChangeMessage">
<thead>
<tr>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="origin" data-type="string" data-visible="false">{{ lang._('Origin') }}</th>
<th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="reqid" data-type="string">{{ lang._('Reqid') }}</th>
<th data-column-id="local" data-type="string">{{ lang._('Local') }}</th>
<th data-column-id="remote" data-type="string">{{ lang._('Remote') }}</th>
<th data-column-id="tunnel_local" data-sortable="false" data-formatter="tunnel">{{ lang._('Tunnel') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-primary"><span class="fa fa-fw fa-plus"></span></button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-fw fa-trash-o"></span></button>
</td>
</tr>
</tfoot>
</table>
<div class="col-md-12">
<div id="VTIChangeMessage" class="alert alert-info" style="display: none" role="alert">
{{ lang._('After changing settings, please remember to apply them with the button below') }}
</div>
<hr/>
</div>
<div class="col-md-12">
<button class="btn btn-primary" id="reconfigureAct"
data-endpoint='/api/ipsec/legacy-subsystem/applyConfig'
data-label="{{ lang._('Apply') }}"
data-error-title="{{ lang._('Error reconfiguring IPsec') }}"
type="button"
></button>
<br/><br/>
</div>
</div>
{{ partial("layout_partials/base_dialog",['fields':formDialogVTI,'id':'DialogVTI','label':lang._('Edit VirtualTunnelInterface')])}}

View File

@ -0,0 +1,50 @@
#!/usr/local/bin/php
<?php
/*
* Copyright (C) 2022 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
require_once("util.inc");
require_once("interfaces.inc");
require_once("config.inc");
require_once("plugins.inc.d/ipsec.inc");
$result = [];
foreach (ipsec_get_configured_vtis() as $vti) {
$record = [
'reqid' => $vti['reqid'],
'local' => $vti['local'],
'remote' => $vti['remote'],
'description' => $vti['descr']
];
if (!empty($vti['networks'])) {
$record['tunnel_local'] = $vti['networks'][0]['tunnel_local'];
$record['tunnel_remote'] = $vti['networks'][0]['tunnel_remote'];
}
$result[] = $record;
}
echo json_encode($result);

View File

@ -28,6 +28,12 @@ parameters:
type:script_output
message:List SAD entries
[list.legacy_vti]
command:/usr/local/opnsense/scripts/ipsec/get_legacy_vti.php
parameters:
type:script_output
message:IPsec list legacy VirtualTunnelInterfaces
[connect]
command:/usr/local/opnsense/scripts/ipsec/connect.py
parameters:%s