From 5752bd6eb38c4e350c7c7227fa310f723069db21 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Mon, 12 Dec 2022 10:37:43 +0100 Subject: [PATCH] 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) --- src/etc/inc/plugins.inc.d/ipsec.inc | 36 +- .../IPsec/Api/ConnectionsController.php | 278 +++++++++++++ .../OPNsense/IPsec/Api/PoolsController.php | 71 ++++ .../IPsec/Api/PreSharedKeysController.php | 2 +- .../OPNsense/IPsec/Api/VtiController.php | 74 ++++ .../OPNsense/IPsec/ConnectionsController.php | 42 ++ .../OPNsense/IPsec/VtiController.php | 38 ++ .../OPNsense/IPsec/forms/dialogChild.xml | 132 ++++++ .../OPNsense/IPsec/forms/dialogConnection.xml | 234 +++++++++++ .../OPNsense/IPsec/forms/dialogLocal.xml | 62 +++ .../OPNsense/IPsec/forms/dialogPSK.xml | 8 +- .../OPNsense/IPsec/forms/dialogPool.xml | 22 + .../OPNsense/IPsec/forms/dialogRemote.xml | 62 +++ .../OPNsense/IPsec/forms/dialogVTI.xml | 47 +++ .../mvc/app/models/OPNsense/IPsec/ACL/ACL.xml | 11 + .../IPsec/FieldTypes/ConnnectionField.php | 81 ++++ .../IPsec/FieldTypes/IKEAdressField.php | 80 ++++ .../IPsec/FieldTypes/IPsecProposalField.php | 62 +++ .../OPNsense/IPsec/FieldTypes/PoolsField.php | 52 +++ .../OPNsense/IPsec/FieldTypes/VTIField.php | 134 ++++++ .../mvc/app/models/OPNsense/IPsec/IPsec.xml | 9 + .../app/models/OPNsense/IPsec/Menu/Menu.xml | 2 + .../mvc/app/models/OPNsense/IPsec/Swanctl.php | 191 +++++++++ .../mvc/app/models/OPNsense/IPsec/Swanctl.xml | 392 ++++++++++++++++++ .../app/views/OPNsense/IPsec/connections.volt | 311 ++++++++++++++ .../views/OPNsense/IPsec/pre_shared_keys.volt | 3 +- .../mvc/app/views/OPNsense/IPsec/vti.volt | 94 +++++ src/opnsense/scripts/ipsec/get_legacy_vti.php | 50 +++ .../service/conf/actions.d/actions_ipsec.conf | 6 + 29 files changed, 2568 insertions(+), 18 deletions(-) create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/ConnectionsController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/PoolsController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/VtiController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/ConnectionsController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/VtiController.php create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogChild.xml create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogConnection.xml create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogLocal.xml create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogPool.xml create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogRemote.xml create mode 100644 src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogVTI.xml create mode 100644 src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/ConnnectionField.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/IKEAdressField.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/IPsecProposalField.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/PoolsField.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/VTIField.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.php create mode 100644 src/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.xml create mode 100644 src/opnsense/mvc/app/views/OPNsense/IPsec/connections.volt create mode 100644 src/opnsense/mvc/app/views/OPNsense/IPsec/vti.volt create mode 100755 src/opnsense/scripts/ipsec/get_legacy_vti.php diff --git a/src/etc/inc/plugins.inc.d/ipsec.inc b/src/etc/inc/plugins.inc.d/ipsec.inc index 05e39015e..2e6f72cae 100644 --- a/src/etc/inc/plugins.inc.d/ipsec.inc +++ b/src/etc/inc/plugins.inc.d/ipsec.inc @@ -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 ( diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/ConnectionsController.php b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/ConnectionsController.php new file mode 100644 index 000000000..6173dbe10 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/ConnectionsController.php @@ -0,0 +1,278 @@ +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); + } + +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/PoolsController.php b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/PoolsController.php new file mode 100644 index 000000000..2bb02673a --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/PoolsController.php @@ -0,0 +1,71 @@ +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); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/PreSharedKeysController.php b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/PreSharedKeysController.php index d6f278747..4a66bee16 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/PreSharedKeysController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/PreSharedKeysController.php @@ -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']); } /** diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/VtiController.php b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/VtiController.php new file mode 100644 index 000000000..2c1bad453 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/Api/VtiController.php @@ -0,0 +1,74 @@ +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); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/ConnectionsController.php b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/ConnectionsController.php new file mode 100644 index 000000000..e273641a9 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/ConnectionsController.php @@ -0,0 +1,42 @@ +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'); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/VtiController.php b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/VtiController.php new file mode 100644 index 000000000..f631c9eea --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/VtiController.php @@ -0,0 +1,38 @@ +view->pick('OPNsense/IPsec/vti'); + $this->view->formDialogVTI = $this->getForm('dialogVTI'); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogChild.xml b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogChild.xml new file mode 100644 index 000000000..abe6eef08 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogChild.xml @@ -0,0 +1,132 @@ +
+ + child.enabled + + checkbox + + + child.connection + + dropdown + + + child.sha256_96 + + checkbox + + 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) + + true + + + child.mode + + dropdown + + 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. + + + + child.policies + + checkbox + + 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 + + true + + + child.start_action + + dropdown + + 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. + + + + child.close_action + + dropdown + true + + 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 + + + + child.dpd_action + + dropdown + + 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. + + + + child.reqid + + text + + 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 + + + + child.esp_proposals + + select_multiple + + + child.local_ts + + select_multiple + + true + List of local traffic selectors to include in CHILD_SA. Each selector is a CIDR subnet definition. + + + child.remote_ts + + select_multiple + + true + List of remote traffic selectors to include in CHILD_SA. Each selector is a CIDR subnet definition. + + + child.rekey_time + + text + + 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 + + true + + + child.description + + text + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogConnection.xml b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogConnection.xml new file mode 100644 index 000000000..cc47bb207 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogConnection.xml @@ -0,0 +1,234 @@ +
+ + connection.uuid + + text + + + + connection.org_uuid + + text + + + + connection.enabled + + checkbox + + + connection.proposals + + select_multiple + + 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. + + + + connection.unique + + dropdown + Connection uniqueness policy to enforce. + To avoid multiple connections from the same user, a uniqueness policy can be enforced. + + true + + + connection.aggressive + + checkbox + true + + 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 + + + + connection.version + + dropdown + + 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 + + + + connection.mobike + + checkbox + + 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 don’t like this behavior, hence it can be disabled + + + + connection.local_addrs + + select_multiple + + true + + 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. + + + + connection.remote_addrs + + select_multiple + + true + + 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. + + + + connection.encap + + checkbox + true + + 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 + + + + connection.reauth_time + + text + true + + 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. + + + + connection.rekey_time + + text + true + + 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 + + + + connection.over_time + + text + true + + 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)] + + + + connection.dpd_delay + + text + + 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 + + + + connection.dpd_timeout + + text + true + + 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 + + + + connection.pools + + select_multiple + + 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. + + + + connection.send_certreq + + checkbox + true + + 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 + + + + connection.send_cert + + dropdown + true + + 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. + + + + connection.keyingtries + + text + true + + 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 + + + + connection.description + + text + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogLocal.xml b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogLocal.xml new file mode 100644 index 000000000..3590e5d4c --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogLocal.xml @@ -0,0 +1,62 @@ +
+ + local.enabled + + checkbox + + + local.connection + + dropdown + + + local.round + + text + Numeric identifier by which authentication rounds are sorted. + + + local.auth + + dropdown + Authentication to perform for this round, when using Pre-Shared key make sure to define one under "VPN->IPsec->Pre-Shared Keys" + + + local.id + + text + 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 certificate’s 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. + + + + local.eap_id + + text + Client EAP-Identity to use in EAP-Identity exchange and the EAP method + + + + local.certs + + select_multiple + List of certificate candidates to use for authentication. + + + local.pubkeys + + select_multiple + List of raw public key candidates to use for authentication. + + + + local.description + + text + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogPSK.xml b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogPSK.xml index 19552c757..27b63716e 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogPSK.xml +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogPSK.xml @@ -1,10 +1,16 @@
preSharedKey.ident - + text This can be either an IP address, fully qualified domain name or an email address. + + preSharedKey.remote_ident + + text + (optional) This can be either an IP address, fully qualified domain name or an email address to identify the remote host. + preSharedKey.Key diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogPool.xml b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogPool.xml new file mode 100644 index 000000000..d8460c217 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogPool.xml @@ -0,0 +1,22 @@ + + + pool.enabled + + checkbox + + + pool.name + + text + + + pool.addrs + + text + + Subnet or range defining addresses allocated in pool. + Accepts a single CIDR subnet defining the pool to allocate addresses from + + + + diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogRemote.xml b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogRemote.xml new file mode 100644 index 000000000..bb1b4b939 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogRemote.xml @@ -0,0 +1,62 @@ +
+ + remote.enabled + + checkbox + + + remote.connection + + dropdown + + + remote.round + + text + Numeric identifier by which authentication rounds are sorted. + + + remote.auth + + dropdown + Authentication to perform for this round + + + remote.id + + text + 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 certificate’s 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. + + + + remote.eap_id + + text + Client EAP-Identity to use in EAP-Identity exchange and the EAP method + + + + remote.certs + + select_multiple + List of certificate candidates to use for authentication. + + + remote.pubkeys + + select_multiple + List of raw public key candidates to use for authentication. + + + + remote.description + + text + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogVTI.xml b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogVTI.xml new file mode 100644 index 000000000..88197647b --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/IPsec/forms/dialogVTI.xml @@ -0,0 +1,47 @@ +
+ + vti.enabled + + checkbox + + + vti.reqid + + text + + This id is used to distinguish traffic and security policies between several if_ipsec interfaces. + + + + vti.local + + text + Local tunnel address used for the outer IP header of ESP packets + + + vti.remote + + text + Remote tunnel address used for the outer IP header of ESP packets + + + vti.tunnel_local + + text + Inner tunnel local address to be used for routing purposes. + + + vti.tunnel_remote + + text + + Inner tunnel remote address to be used for routing purposes. + The size of the subnet containing local and remote will be calculated automatically + + + + vti.description + + text + +
diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/IPsec/ACL/ACL.xml index 31bb48aa4..540b803b0 100644 --- a/src/opnsense/mvc/app/models/OPNsense/IPsec/ACL/ACL.xml +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/ACL/ACL.xml @@ -17,6 +17,17 @@ api/ipsec/legacy-subsystem/* + + VPN: IPsec connections [new] + + ui/ipsec/connections + ui/ipsec/vti + api/ipsec/connections/* + api/ipsec/pools/* + api/ipsec/vti/* + api/ipsec/legacy-subsystem/* + + diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/ConnnectionField.php b/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/ConnnectionField.php new file mode 100644 index 000000000..8c35d50f2 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/ConnnectionField.php @@ -0,0 +1,81 @@ +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(); + } + +} diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/IKEAdressField.php b/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/IKEAdressField.php new file mode 100644 index 000000000..ee90d7929 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/IKEAdressField.php @@ -0,0 +1,80 @@ +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; + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/IPsecProposalField.php b/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/IPsecProposalField.php new file mode 100644 index 000000000..3971c8ea2 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/IPsecProposalField.php @@ -0,0 +1,62 @@ +internalOptionList = self::$internalCacheOptionList; + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/PoolsField.php b/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/PoolsField.php new file mode 100644 index 000000000..934573c6d --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/PoolsField.php @@ -0,0 +1,52 @@ +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; + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/VTIField.php b/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/VTIField.php new file mode 100644 index 000000000..c3bf0ad44 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/FieldTypes/VTIField.php @@ -0,0 +1,134 @@ +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; + } + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.xml b/src/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.xml index 5b89e5b35..4ad02defd 100644 --- a/src/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.xml +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/IPsec.xml @@ -43,9 +43,18 @@ Another entry with the same identifier already exists. UniqueConstraint + remote_ident + + N + /^([a-zA-Z0-9@\.\-]*)/u + The identifier contains invalid characters. + + ident.check001 + + Y PSK diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/IPsec/Menu/Menu.xml index 5a38420db..22007424c 100644 --- a/src/opnsense/mvc/app/models/OPNsense/IPsec/Menu/Menu.xml +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/Menu/Menu.xml @@ -1,6 +1,7 @@ + @@ -15,6 +16,7 @@ + diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.php b/src/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.php new file mode 100644 index 000000000..f8487d8f3 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.php @@ -0,0 +1,191 @@ +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; + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.xml b/src/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.xml new file mode 100644 index 000000000..6d4bea5ea --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/IPsec/Swanctl.xml @@ -0,0 +1,392 @@ + + //OPNsense/Swanctl + 1.0.0 + OPNsense IPsec Connections + + + + + 1 + Y + + + default + Y + + + Y + no + + No (default) + Never + Keep + Replace + + + + 0 + Y + + + Y + 0 + + IKEv1+IKEv2 + IKEv1 + IKEv2 + + + + 1 + Y + + + N + + + N + + + 0 + Y + + + 0 + 500000 + N + + + 0 + 500000 + N + + + 0 + 500000 + N + + + 0 + 500000 + N + + + 0 + 500000 + N + + + N + Y + + + 1 + Y + + + N + Default + + If asked + Never + Always + + + + 0 + 1000 + N + + + N + + + + + + + 1 + Y + + + + + OPNsense.IPsec.Swanctl + Connections.Connection + description + + + Y + + + Y + 0 + 10 + 0 + + + Y + psk + + Pre-Shared Key + Public Key + EAP TLS + EAP-MSCHAPv2 + Xauth PAM + EAP RADIUS + + + + N + /^([0-9a-zA-Z.\-,_\:){0,4196}$/u + + + N + /^([0-9a-zA-Z.\-,_\:){0,4196}$/u + + + N + Y + Please select a valid certificate from the list + + + + + OPNsense.IPsec.IPsec + keyPairs.keyPair + name + + + Y + N + + + N + + + + + + + 1 + Y + + + + + OPNsense.IPsec.Swanctl + Connections.Connection + description + + + Y + + + Y + 0 + 10 + 0 + + + Y + psk + + Pre-Shared Key + Public Key + EAP TLS + EAP-MSCHAPv2 + Xauth PAM + EAP RADIUS + + + + N + /^([0-9a-zA-Z.\-,_\:){0,4196}$/u + + + N + /^([0-9a-zA-Z.\-,_\:){0,4196}$/u + + + N + Y + Please select a valid certificate from the list + + + + + OPNsense.IPsec.IPsec + keyPairs.keyPair + name + + + Y + N + + + N + + + + + + + 1 + Y + + + + + OPNsense.IPsec.Swanctl + Connections.Connection + description + + + Y + + + 1 + 65535 + N + + + default + Y + + + 0 + Y + + + Y + start + + None + Trap+start + Route + Start + Trap + + + + Y + none + + None + Trap + Start + + + + Y + clear + + Clear + Trap + Start + + + + Y + tunnel + + Tunnel + Transport + Pass + Drop + + + + Y + , + Y + N + + + Y + , + Y + N + + + 3600 + 0 + 500000 + Y + + + N + + + + + + + 1 + Y + + + N + + + Pool name must be unique. + UniqueConstraint + + + + + Y + N + Y + Please specify a valid CIDR subnet. + + + + + + + 1 + Y + + + 1 + 65535 + Y + + + Reqid must be unique. + UniqueConstraint + + + + + N + N + Y + Please specify a valid address. + + + N + N + Y + Please specify a valid address. + + + N + N + Y + Please specify a valid address. + + + N + N + Y + Please specify a valid address. + + + N + + + + + diff --git a/src/opnsense/mvc/app/views/OPNsense/IPsec/connections.volt b/src/opnsense/mvc/app/views/OPNsense/IPsec/connections.volt new file mode 100644 index 000000000..d2483cf04 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/IPsec/connections.volt @@ -0,0 +1,311 @@ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Description') }}{{ lang._('Local') }}{{ lang._('Remote') }}{{ lang._('Local Nets') }}{{ lang._('Remote Nets') }}{{ lang._('Commands') }}
+ + +
+
+ +
+
+
+ +

+
+
+
+
+

{{ lang._('General settings')}}

+
+
+
+
+
+
+
+
+
+
+
+

{{ lang._('Local Authentication')}}

+
+
+ + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Round') }}{{ lang._('Authentication') }}{{ lang._('Description') }}{{ lang._('Commands') }}
+ +
+
+
+
+

{{ lang._('Remote Authentication')}}

+
+
+ + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Round') }}{{ lang._('Authentication') }}{{ lang._('Description') }}{{ lang._('Commands') }}
+ +
+
+
+
+
+
+

{{ lang._('Children')}}

+
+
+ + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Description') }}{{ lang._('Local Nets') }}{{ lang._('Remote Nets') }}{{ lang._('Commands') }}
+ + +
+
+
+
+
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Name') }}{{ lang._('Commands') }}
+ + +
+
+ +
+
+
+
+ + + +{{ 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')])}} diff --git a/src/opnsense/mvc/app/views/OPNsense/IPsec/pre_shared_keys.volt b/src/opnsense/mvc/app/views/OPNsense/IPsec/pre_shared_keys.volt index 8121cc3f6..38c504baf 100644 --- a/src/opnsense/mvc/app/views/OPNsense/IPsec/pre_shared_keys.volt +++ b/src/opnsense/mvc/app/views/OPNsense/IPsec/pre_shared_keys.volt @@ -16,7 +16,8 @@ {{ lang._('ID') }} - {{ lang._('Identifier') }} + {{ lang._('Local Identifier') }} + {{ lang._('Remote Identifier') }} {{ lang._('Key Type') }} {{ lang._('Commands') }} diff --git a/src/opnsense/mvc/app/views/OPNsense/IPsec/vti.volt b/src/opnsense/mvc/app/views/OPNsense/IPsec/vti.volt new file mode 100644 index 000000000..dfe84f786 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/IPsec/vti.volt @@ -0,0 +1,94 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Origin') }}{{ lang._('Enabled') }}{{ lang._('Reqid') }}{{ lang._('Local') }}{{ lang._('Remote') }}{{ lang._('Tunnel') }}{{ lang._('Description') }}{{ lang._('Commands') }}
+ + +
+
+ +
+
+
+ +

+
+
+ + + +{{ partial("layout_partials/base_dialog",['fields':formDialogVTI,'id':'DialogVTI','label':lang._('Edit VirtualTunnelInterface')])}} diff --git a/src/opnsense/scripts/ipsec/get_legacy_vti.php b/src/opnsense/scripts/ipsec/get_legacy_vti.php new file mode 100755 index 000000000..a5e89488d --- /dev/null +++ b/src/opnsense/scripts/ipsec/get_legacy_vti.php @@ -0,0 +1,50 @@ +#!/usr/local/bin/php + $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); diff --git a/src/opnsense/service/conf/actions.d/actions_ipsec.conf b/src/opnsense/service/conf/actions.d/actions_ipsec.conf index 7b4f2044a..83830b0ae 100644 --- a/src/opnsense/service/conf/actions.d/actions_ipsec.conf +++ b/src/opnsense/service/conf/actions.d/actions_ipsec.conf @@ -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