%PDF- %PDF-
| Direktori : /proc/self/root/backups/router/usr/local/etc/inc/plugins.inc.d/ |
| Current File : //proc/self/root/backups/router/usr/local/etc/inc/plugins.inc.d/ipsec.inc |
<?php
/*
* Copyright (C) 2016-2023 Deciso B.V.
* Copyright (C) 2019 Pascal Mathis <mail@pascalmathis.com>
* Copyright (C) 2008 Shrew Soft Inc. <mgrooms@shrew.net>
* Copyright (C) 2008 Ermal Luçi
* Copyright (C) 2004-2007 Scott Ullrich <sullrich@gmail.com>
* Copyright (C) 2003-2004 Manuel Kasper <mk@neon1.net>
* 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.
*/
const IPSEC_LOG_SUBSYSTEMS = [
'asn' => 'Low-level encoding/decoding (ASN.1, X.509 etc.)',
'cfg' => 'Configuration management and plugins',
'chd' => 'CHILD_SA/IPsec SA',
'dmn' => 'Main daemon setup/cleanup/signal handling',
'enc' => 'Packet encoding/decoding encryption/decryption operations',
'esp' => 'libipsec library messages',
'ike' => 'IKE_SA/ISAKMP SA',
'imc' => 'Integrity Measurement Collector',
'imv' => 'Integrity Measurement Verifier',
'job' => 'Jobs queuing/processing and thread pool management',
'knl' => 'IPsec/Networking kernel interface',
'lib' => 'libstrongwan library messages',
'mgr' => 'IKE_SA manager, handling synchronization for IKE_SA access',
'net' => 'IKE network communication',
'pts' => 'Platform Trust Service',
'tls' => 'libtls library messages',
'tnc' => 'Trusted Network Connect',
];
const IPSEC_LOG_LEVELS = [
-1 => 'Silent',
0 => 'Basic',
1 => 'Audit',
2 => 'Control',
3 => 'Raw',
4 => 'Highest',
];
function ipsec_p1_ealgos()
{
return array(
'aes' => array( 'name' => 'AES', 'keysel' => array( 'lo' => 128, 'hi' => 256, 'step' => 64 ), 'iketype' => null ),
'aes128gcm16' => array( 'name' => '128 bit AES-GCM with 128 bit ICV', 'iketype' => null ),
'aes192gcm16' => array( 'name' => '192 bit AES-GCM with 128 bit ICV', 'iketype' => null ),
'aes256gcm16' => array( 'name' => '256 bit AES-GCM with 128 bit ICV', 'iketype' => null ),
'camellia' => array( 'name' => 'Camellia', 'keysel' => array( 'lo' => 128, 'hi' => 256, 'step' => 64 ), 'iketype' => 'ikev2' ),
'blowfish' => array( 'name' => 'Blowfish', 'keysel' => array( 'lo' => 128, 'hi' => 256, 'step' => 64 ), 'iketype' => null ),
'3des' => array( 'name' => '3DES', 'iketype' => null ),
'cast128' => array( 'name' => 'CAST128', 'iketype' => null ),
'des' => array( 'name' => 'DES', 'iketype' => null )
);
}
function ipsec_p1_authentication_methods()
{
return array(
'hybrid_rsa_server' => array( 'name' => 'Hybrid RSA + Xauth', 'mobile' => true ),
'xauth_rsa_server' => array( 'name' => 'Mutual RSA + Xauth', 'mobile' => true ),
'xauth_psk_server' => array( 'name' => 'Mutual PSK + Xauth', 'mobile' => true ),
'eap-tls' => array( 'name' => 'EAP-TLS', 'mobile' => true),
'psk_eap-tls' => array( 'name' => 'RSA (local) + EAP-TLS (remote)', 'mobile' => true),
'eap-mschapv2' => array( 'name' => 'EAP-MSCHAPV2', 'mobile' => true),
'rsa_eap-mschapv2' => array( 'name' => 'Mutual RSA + EAP-MSCHAPV2', 'mobile' => true),
'eap-radius' => array( 'name' => 'EAP-RADIUS', 'mobile' => true),
'rsasig' => array( 'name' => 'Mutual RSA', 'mobile' => false ),
'pubkey' => array( 'name' => 'Mutual Public Key', 'mobile' => false ),
'pre_shared_key' => array( 'name' => 'Mutual PSK', 'mobile' => false ),
);
}
function ipsec_p2_ealgos()
{
return array(
'aes128' => array( 'name' => 'aes', 'keylen' => "128", 'descr' => gettext("AES128")),
'aes192' => array( 'name' => 'aes', 'keylen' => "192", 'descr' => gettext("AES192")),
'aes256' => array( 'name' => 'aes', 'keylen' => "256", 'descr' => gettext("AES256")),
'aes128gcm16' => array( 'name' => 'aes128gcm16', 'descr' => 'aes128gcm16'),
'aes192gcm16' => array( 'name' => 'aes192gcm16', 'descr' => 'aes192gcm16'),
'aes256gcm16' => array( 'name' => 'aes256gcm16', 'descr' => 'aes256gcm16'),
'null' => array( 'name' => 'null', 'descr' => gettext("NULL (no encryption)"))
);
}
function ipsec_p2_halgos()
{
return array(
'hmac_sha1' => 'SHA1',
'hmac_sha256' => 'SHA256',
'hmac_sha384' => 'SHA384',
'hmac_sha512' => 'SHA512',
'aesxcbc' => 'AES-XCBC'
);
}
function ipsec_configure()
{
return [
'ipsec' => ['ipsec_configure_do:2'],
'vpn' => ['ipsec_configure_do:2'],
];
}
function ipsec_syslog()
{
$logfacilities = [];
$logfacilities['ipsec'] = array(
'facility' => array('charon'),
);
return $logfacilities;
}
function ipsec_services()
{
global $config;
$services = [];
if (!empty($config['ipsec']['enable']) || (new \OPNsense\IPsec\Swanctl())->isEnabled()) {
$pconfig = [];
$pconfig['name'] = 'strongswan';
$pconfig['description'] = gettext('IPsec VPN');
$pconfig['pidfile'] = '/var/run/charon.pid';
$pconfig['configd'] = array(
'restart' => array('ipsec restart'),
'start' => array('ipsec start'),
'stop' => array('ipsec stop'),
);
$services[] = $pconfig;
}
return $services;
}
function ipsec_interfaces()
{
global $config;
$swanctl = (new \OPNsense\IPsec\Swanctl());
$interfaces = [];
$is_enabled = $swanctl->isEnabled();
if (!$is_enabled && isset($config['ipsec']['phase1'])) {
foreach ($config['ipsec']['phase1'] as $ph1ent) {
if (empty($ph1ent['disabled'])) {
$is_enabled = true;
break;
}
}
}
if ($is_enabled) {
$oic = ['enable' => true];
$oic['if'] = 'enc0';
$oic['descr'] = 'IPsec';
$oic['type'] = 'none';
$oic['virtual'] = true;
$interfaces['enc0'] = $oic;
$vtis = array_merge(ipsec_get_configured_vtis(), $swanctl->getVtiDevices());
foreach ($vtis as $intf => $details) {
$interfaces[$intf] = [
'enable' => true,
'descr' => preg_replace('/[^a-z_0-9]/i', '', $details['descr']),
'if' => $intf,
'type' => 'none',
];
}
}
return $interfaces;
}
function ipsec_devices()
{
$devices = [];
$names = [];
$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']),
'name' => $device,
];
}
$devices[] = [
'function' => 'ipsec_configure_device',
'configurable' => false,
'pattern' => '^ipsec',
'volatile' => true,
'names' => $names,
'type' => 'ipsec',
];
$devices[] = ['pattern' => '^enc', 'volatile' => true];
return $devices;
}
function ipsec_firewall(\OPNsense\Firewall\Plugin $fw)
{
global $config;
if (
(string)(new \OPNsense\IPsec\IPsec())->general->disablevpnrules == '0' &&
isset($config['ipsec']['enable']) && isset($config['ipsec']['phase1'])
) {
$enable_replyto = empty($config['system']['disablereplyto']);
$enable_routeto = empty($config['system']['pf_disable_force_gw']);
foreach ($config['ipsec']['phase1'] as $ph1ent) {
if (!isset($ph1ent['disabled'])) {
// detect remote ip
$rgip = null;
if (isset($ph1ent['mobile'])) {
$rgip = "any";
} elseif (!is_ipaddr($ph1ent['remote-gateway'])) {
$rgip = ipsec_resolve($ph1ent['remote-gateway'], $ph1ent['protocol']);
} else {
$rgip = $ph1ent['remote-gateway'];
}
if (!empty($rgip)) {
$protos_used = [];
if (is_array($config['ipsec']['phase2'])) {
foreach ($config['ipsec']['phase2'] as $ph2ent) {
if ($ph2ent['ikeid'] == $ph1ent['ikeid']) {
if ($ph2ent['protocol'] == 'esp' || $ph2ent['protocol'] == 'ah') {
if (!in_array($ph2ent['protocol'], $protos_used)) {
$protos_used[] = $ph2ent['protocol'];
}
}
}
}
}
$interface = explode('_vip', $ph1ent['interface'])[0];
$baserule = array("interface" => $interface,
"log" => !isset($config['syslog']['nologdefaultpass']),
"quick" => false,
"type" => "pass",
"statetype" => "keep",
"#ref" => "ui/ipsec/connections/settings",
"descr" => "IPsec: " . (!empty($ph1ent['descr']) ? $ph1ent['descr'] : $rgip)
);
// find gateway
$gwname = $fw->getGateways()->getInterfaceGateway($interface, $ph1ent['protocol'], true, 'name');
if ((strpos($rgip, ':') === false ? 'inet' : 'inet6') != $ph1ent['protocol']) {
// XXX: prevent route-to when gateway and remote address family mismatch
$enable_routeto = false;
}
// register rules
$fw->registerFilterRule(
500000,
array("direction" => "out", "protocol" => "udp", "to" => $rgip, "to_port" => 500,
"gateway" => $enable_routeto ? $gwname : null, "disablereplyto" => true),
$baserule
);
$fw->registerFilterRule(
500000,
array("direction" => "in", "protocol" => "udp", "from" => $rgip, "to_port" => 500,
"reply-to" => $enable_replyto ? $gwname : null),
$baserule
);
if ($ph1ent['nat_traversal'] != "off") {
$fw->registerFilterRule(
500000,
array("direction" => "out", "protocol" => "udp", "to" => $rgip, "to_port" => 4500,
"gateway" => $enable_routeto ? $gwname : null, "disablereplyto" => true),
$baserule
);
$fw->registerFilterRule(
500000,
array("direction" => "in", "protocol" => "udp", "from" => $rgip, "to_port" => 4500,
"reply-to" => $enable_replyto ? $gwname : null),
$baserule
);
}
foreach ($protos_used as $proto) {
$fw->registerFilterRule(
500000,
array("direction" => "out", "protocol" => $proto, "to" => $rgip,
"gateway" => $enable_routeto ? $gwname : null, "disablereplyto" => true),
$baserule
);
$fw->registerFilterRule(
500000,
array("direction" => "in", "protocol" => $proto, "from" => $rgip,
"reply-to" => $enable_replyto ? $gwname : null),
$baserule
);
}
}
}
}
}
}
function ipsec_xmlrpc_sync()
{
$result = [];
$result[] = array(
'description' => gettext('IPsec'),
'section' => 'ipsec,OPNsense.IPsec,OPNsense.Swanctl',
'id' => 'ipsec',
'services' => ["strongswan"],
);
return $result;
}
/*
* Return phase1 local address
*/
function ipsec_get_phase1_src(&$ph1ent)
{
if (!empty($ph1ent['interface'])) {
if ($ph1ent['interface'] == 'any') {
return '%any';
} elseif (!is_ipaddr($ph1ent['interface'])) {
$if = $ph1ent['interface'];
} else {
// interface is an ip address, return
return $ph1ent['interface'];
}
} else {
$if = "wan";
}
if ($ph1ent['protocol'] == "inet46") {
$ipv4 = get_interface_ip($if);
$ipv6 = get_interface_ipv6($if);
return $ipv4 ? $ipv4 . ($ipv6 ? ',' . $ipv6 : '') : $ipv6;
} elseif ($ph1ent['protocol'] == "inet6") {
return get_interface_ipv6($if);
} else {
return get_interface_ip($if);
}
}
/**
* Unravel the logic in phase 2 parsing, tunnels can either be merged or isolated depending on the type.
* This function parses the phase2 entries with (all of) it's weirdness so we can safely use this to construct
* connection entries.
*
* Without breaking existing setups, we can't easily push the choices back to the user, because most of the
* weirdness is a result of improper input handling.
* (should ease future migration if needed as well.)
*/
function ipsec_parse_phase2($ikeid)
{
global $config;
$result = [
"type" => "tunnel", // strongswan's default when type is not specified
"local_ts" => [],
"remote_ts" => [],
"ah_proposals" => [],
"esp_proposals" => [],
"life_time" => [],
"rekey_time" => [],
"rand_time" => [],
"reqids" => [],
"uniqid_reqid" => []
];
$a_phase1 = isset($config['ipsec']['phase1']) ? $config['ipsec']['phase1'] : [];
$a_phase2 = isset($config['ipsec']['phase2']) ? $config['ipsec']['phase2'] : [];
$a_client = isset($config['ipsec']['client']) ? $config['ipsec']['client'] : [];
$ph1ent = current(array_filter($a_phase1, function ($e) use ($ikeid) {
return $e['ikeid'] == $ikeid;
}));
if ($ph1ent) {
$uniqids = [];
$keyexchange = !empty($ph1ent['iketype']) ? $ph1ent['iketype'] : "ikev1";
$idx = 0;
foreach ($a_phase2 as $ph2ent) {
if ($ph1ent['ikeid'] != $ph2ent['ikeid'] || isset($ph2ent['disabled'])) {
continue;
} elseif (isset($ph1ent['mobile']) && !isset($a_client['enable'])) {
continue;
} elseif (in_array($ph2ent['mode'], ['tunnel', 'tunnel6'])) {
$leftsubnet_data = ipsec_idinfo_to_cidr($ph2ent['localid'], false, $ph2ent['mode']);
if (!is_ipaddr($leftsubnet_data) && !is_subnet($leftsubnet_data) && ($leftsubnet_data != "0.0.0.0/0")) {
log_msg("Invalid IPsec Phase 2 \"{$ph2ent['descr']}\" - {$ph2ent['localid']['type']} has no subnet.", LOG_ERR);
continue;
}
$result['local_ts'][] = $leftsubnet_data;
if (!isset($ph1ent['mobile'])) {
$result['remote_ts'][] = ipsec_idinfo_to_cidr($ph2ent['remoteid'], false, $ph2ent['mode']);
}
} elseif ($ph2ent['mode'] == 'route-based') {
if (is_ipaddrv6($ph2ent['tunnel_local'])) {
$result['local_ts'][] = '::/0';
$result['remote_ts'][] = '::/0';
} else {
$result['local_ts'][] = '0.0.0.0/0';
$result['remote_ts'][] = '0.0.0.0/0';
}
} else {
$result['type'] = 'transport';
if (
!((($ph1ent['authentication_method'] == "xauth_psk_server") ||
($ph1ent['authentication_method'] == "pre_shared_key")) && isset($ph1ent['mobile']))
) {
$result['local_ts'][] = ipsec_get_phase1_src($ph1ent);
}
if (!isset($ph1ent['mobile'])) {
$result['remote_ts'][] = $ph1ent['remote-gateway'];
}
}
$uniqids[] = $ph2ent['uniqid'];
if (isset($ph1ent['mobile']) && isset($a_client['pfs_group'])) {
$ph2ent['pfsgroup'] = $a_client['pfs_group'];
}
if (isset($ph2ent['protocol']) && $ph2ent['protocol'] == 'esp') {
$ealgoESPsp2arr_details = [];
if (is_array($ph2ent['encryption-algorithm-option'])) {
foreach ($ph2ent['encryption-algorithm-option'] as $algidx => $ealg) {
foreach (ipsec_p2_ealgos() as $algo_name => $algo_data) {
$tmpealgo = "";
if ($algo_name == $ealg['name']) {
$tmpealgo = $algo_name;
} elseif ($algo_data['name'] == $ealg['name']) {
// XXX: extract and convert legacy encryption-algorithm-option setting
// (pre 22.1 saved values)
if ($ealg['keylen'] == $algo_data['keylen'] || $ealg['keylen'] == "auto") {
$tmpealgo = $algo_name;
}
}
if (!empty($tmpealgo)) {
$modp = isset($ph2ent['pfsgroup']) ? ipsec_convert_to_modp($ph2ent['pfsgroup']) : null;
if (is_array($ph2ent['hash-algorithm-option'])) {
foreach (array_keys(ipsec_p2_halgos()) as $halgo) {
if (in_array($halgo, $ph2ent['hash-algorithm-option'])) {
$ealgoESPsp2arr_details[] = sprintf(
"%s-%s%s",
$tmpealgo,
str_replace('hmac_', '', $halgo),
!empty($modp) ? "-{$modp}" : ""
);
}
}
} else {
$ealgoESPsp2arr_details[] = $tmpealgo . (!empty($modp) ? "-{$modp}" : "");
}
}
}
}
}
$result['esp_proposals'][] = $ealgoESPsp2arr_details;
} elseif (isset($ph2ent['protocol']) && $ph2ent['protocol'] == 'ah') {
$ealgoAHsp2arr_details = [];
if (!empty($ph2ent['hash-algorithm-option']) && is_array($ph2ent['hash-algorithm-option'])) {
$modp = ipsec_convert_to_modp($ph2ent['pfsgroup']);
foreach ($ph2ent['hash-algorithm-option'] as $tmpAHalgo) {
$tmpAHalgo = str_replace('hmac_', '', $tmpAHalgo);
if (!empty($modp)) {
$tmpAHalgo = "-{$modp}";
}
$ealgoAHsp2arr_details[] = $tmpAHalgo;
}
}
$result['ah_proposals'][] = $ealgoAHsp2arr_details;
}
if (isset($ph1ent['rekey_enable'])) {
$result['life_time'][] = 0;
} else {
$result['life_time'][] = !empty($ph2ent['lifetime']) ? $ph2ent['lifetime'] : null;
}
if (!empty($ph1ent['margintime']) && !empty($ph1ent['rekeyfuzz']) && !empty($ph2ent['lifetime'])) {
$result['rekey_time'][] = ($ph2ent['lifetime'] - $ph1ent['margintime']);
$result['rand_time'][] = ($ph1ent['margintime'] * $ph1ent['rekeyfuzz']);
} else {
$result['rekey_time'][] = null;
$result['rand_time'][] = null;
}
$result['reqids'][] = !empty($ph2ent['reqid']) ? $ph2ent['reqid'] : null;
$idx++;
}
if ((!isset($ph1ent['mobile']) && $keyexchange == 'ikev1') || isset($ph1ent['tunnel_isolation'])) {
// isolated tunnels
for ($idx = 0; $idx < count($result['local_ts']); ++$idx) {
$result['uniqid_reqid'][$uniqids[$idx]] = $result['reqids'][$idx];
}
} else {
// merge tunnels
if (!empty($result['reqids'])) {
$result['reqids'] = [min($result['reqids'])];
for ($idx = 0; $idx < count($result['local_ts']); ++$idx) {
$result['uniqid_reqid'][$uniqids[$idx]] = $result['reqids'][0];
}
}
$result['local_ts'] = array_unique($result['local_ts']);
$result['remote_ts'] = array_unique($result['remote_ts']);
// merge esp phase 2 arrays.
$esp_content = [];
foreach ($result['esp_proposals'] as $ealgoESPsp2arr_details) {
foreach ($ealgoESPsp2arr_details as $esp_item) {
if (!in_array($esp_item, $esp_content)) {
$esp_content[] = $esp_item;
}
}
}
$result['esp_proposals'] = $esp_content;
// merge ah phase 2 arrays.
$ah_content = [];
foreach ($result['ah_proposals'] as $ealgoAHsp2arr_details) {
foreach ($ealgoAHsp2arr_details as $ah_item) {
if (!in_array($ah_item, $ah_content)) {
$ah_content[] = $ah_item;
}
}
}
$result['ah_proposals'] = $ah_content;
}
}
return $result;
}
/*
* Return phase2 idinfo in cidr format
*/
function ipsec_idinfo_to_cidr(&$idinfo, $addrbits = false, $mode = '')
{
switch ($idinfo['type'] ?? 'none') {
case "address":
if ($addrbits) {
if ($mode == "tunnel6") {
return $idinfo['address'] . "/128";
} else {
return $idinfo['address'] . "/32";
}
} else {
return $idinfo['address'];
}
break; /* NOTREACHED */
case "network":
return "{$idinfo['address']}/{$idinfo['netbits']}";
break; /* NOTREACHED */
case "none":
case "mobile":
return "0.0.0.0/0";
break; /* NOTREACHED */
default:
if (empty($mode) && !empty($idinfo['mode'])) {
$mode = $idinfo['mode'];
}
if ($mode == 'tunnel6') {
list (, $network) = interfaces_routed_address6($idinfo['type']);
return $network;
} else {
list (, $network) = interfaces_primary_address($idinfo['type']);
return $network;
}
break; /* NOTREACHED */
}
}
/*
* Return phase1 association for phase2
*/
function ipsec_lookup_phase1(&$ph2ent, &$ph1ent)
{
global $config;
if (!isset($config['ipsec']) || !is_array($config['ipsec'])) {
return false;
}
if (!is_array($config['ipsec']['phase1'])) {
return false;
}
if (empty($config['ipsec']['phase1'])) {
return false;
}
foreach ($config['ipsec']['phase1'] as $ph1tmp) {
if ($ph1tmp['ikeid'] == $ph2ent['ikeid']) {
$ph1ent = $ph1tmp;
return $ph1ent;
}
}
return false;
}
/*
* Check phase1 communications status
*/
function ipsec_phase1_status($ipsec_status, $ikeid)
{
foreach ($ipsec_status as $ike) {
if ($ike['id'] != $ikeid) {
continue;
}
if ($ike['status'] == 'established') {
return true;
}
break;
}
return false;
}
function ipsec_resolve($hostname, $ipproto = 'inet')
{
if (!is_ipaddr($hostname)) {
$dns_qry_type = $ipproto == 'inet6' ? DNS_AAAA : DNS_A;
$dns_qry_outfield = $ipproto == 'inet6' ? "ipv6" : "ip";
$dns_records = @dns_get_record($hostname, $dns_qry_type);
if (is_array($dns_records)) {
foreach ($dns_records as $dns_record) {
if (!empty($dns_record[$dns_qry_outfield])) {
return $dns_record[$dns_qry_outfield];
}
}
}
return null;
}
return $hostname;
}
function ipsec_find_id(&$ph1ent, $side = 'local')
{
if ($side == "local") {
$id_type = $ph1ent['myid_type'] ?? null;
$id_data = isset($ph1ent['myid_data']) ? $ph1ent['myid_data'] : null;
} elseif ($side == "peer") {
$id_type = $ph1ent['peerid_type'] ?? null;
$id_data = isset($ph1ent['peerid_data']) ? $ph1ent['peerid_data'] : null;
/* Only specify peer ID if we are not dealing with a mobile PSK-only tunnel */
if (isset($ph1ent['mobile'])) {
return null;
}
} else {
return null;
}
if ($id_type == "myaddress") {
$thisid_data = ipsec_get_phase1_src($ph1ent);
} elseif ($id_type == "dyn_dns") {
$thisid_data = ipsec_resolve($id_data);
} elseif ($id_type == "peeraddress") {
$thisid_data = ipsec_resolve($ph1ent['remote-gateway']);
} elseif (empty($id_data)) {
$thisid_data = null;
} elseif (in_array($id_type, ["asn1dn", "fqdn"])) {
if (strpos($id_data, "#") !== false) {
$thisid_data = "\"{$id_type}:{$id_data}\"";
} else {
$thisid_data = "{$id_type}:{$id_data}";
}
} elseif ($id_type == "keyid tag") {
$thisid_data = "keyid:{$id_data}";
} elseif ($id_type == "user_fqdn") {
$thisid_data = "userfqdn:{$id_data}";
} else {
$thisid_data = $id_data;
}
return trim($thisid_data);
}
/* include all configuration functions */
function ipsec_convert_to_modp($index): string
{
$map = [
1 => 'modp768',
2 => 'modp1024',
5 => 'modp1536',
14 => 'modp2048',
15 => 'modp3072',
16 => 'modp4096',
17 => 'modp6144',
18 => 'modp8192',
19 => 'ecp256',
20 => 'ecp384',
21 => 'ecp521',
22 => 'modp1024s160',
23 => 'modp2048s224',
24 => 'modp2048s256',
28 => 'ecp256bp',
29 => 'ecp384bp',
30 => 'ecp512bp',
31 => 'curve25519',
];
if (!array_key_exists($index, $map)) {
return '';
}
return $map[$index];
}
/**
* load manual defined spd entries using setkey
*/
function ipsec_configure_spd()
{
global $config;
$spd_entries = [];
/**
* try to figure out which SPD entries where created manually and collect them in a list sequenced by reqid
* when the entry is either bound to an interface (ifname=) or has a lifetime, these where not likely created by us
*/
exec('/sbin/setkey -PD', $lines);
$line_count = 0;
$src = $dst = $direction = $reqid = '';
$is_automatic = false;
$previous_spd_entries = [];
foreach ($lines as $line) {
if ($line[0] != "\t") {
$tmp = explode(' ', $line);
$src = $dst = $direction = $reqid = '';
if (count($tmp) >= 3) {
$src = explode('[', $tmp[0])[0];
$dst = explode('[', $tmp[1])[0];
}
$line_count = 0;
$is_automatic = false;
} elseif ($line_count == 1) {
// direction
$direction = trim(explode(' ', $line)[0]);
} elseif (strpos($line, '/unique:') !== false) {
$reqid = explode('/unique:', $line)[1];
} elseif (strpos($line, "\tlifetime:") === 0 || strpos($line, "ifname=") > 0) {
$is_automatic = true;
} elseif (strpos($line, "\trefcnt") === 0 && !$is_automatic && $reqid != "") {
if (empty($previous_spd_entries[$reqid])) {
$previous_spd_entries[$reqid] = [];
}
$previous_spd_entries[$reqid][] = sprintf("spddelete -n %s %s any -P %s;", $src, $dst, $direction);
}
$line_count++;
}
// process manual added spd entries
if (!empty($config['ipsec']['phase1']) && !empty($config['ipsec']['phase2'])) {
foreach ($config['ipsec']['phase1'] as $ph1ent) {
if (!empty($ph1ent['disabled'])) {
continue;
}
$reqid_mapping = ipsec_parse_phase2($ph1ent['ikeid'])['uniqid_reqid'];
foreach (array_unique(array_values($reqid_mapping)) as $reqid) {
if (!empty($previous_spd_entries[$reqid])) {
foreach ($previous_spd_entries[$reqid] as $spd_entry) {
$spd_entries[] = $spd_entry;
}
unset($previous_spd_entries[$reqid]);
}
}
foreach ($config['ipsec']['phase2'] as $ph2ent) {
if (!isset($ph2ent['disabled']) && $ph1ent['ikeid'] == $ph2ent['ikeid'] && !empty($ph2ent['spd']) && !empty($ph2ent['remoteid'])) {
$tunnel_src = ipsec_get_phase1_src($ph1ent);
$tunnel_dst = ipsec_resolve($ph1ent['remote-gateway'], $ph1ent['protocol']);
if (empty($tunnel_dst) || empty($tunnel_src)) {
log_msg(sprintf(
"spdadd: skipped for tunnel %s-%s (reqid :%s, local: %s, remote: %s)",
$tunnel_src,
$tunnel_dst,
!empty($reqid_mapping[$ph2ent['uniqid']]) ? $reqid_mapping[$ph2ent['uniqid']] : "",
$ph2ent['spd'],
ipsec_idinfo_to_cidr($ph2ent['remoteid'], false, $ph2ent['mode'])
), LOG_ERR);
continue;
}
foreach (explode(',', $ph2ent['spd']) as $local_net) {
$proto = $ph2ent['mode'] == "tunnel" ? "4" : "6";
$remote_net = ipsec_idinfo_to_cidr($ph2ent['remoteid'], false, $ph2ent['mode']);
if (!empty($reqid_mapping[$ph2ent['uniqid']])) {
$req_id = $reqid_mapping[$ph2ent['uniqid']];
$spd_command = "spdadd -%s %s %s any -P out ipsec %s/tunnel/%s-%s/unique:{$req_id};";
$spd_entries[] = sprintf(
$spd_command,
$proto,
trim($local_net),
$remote_net,
$ph2ent['protocol'],
$tunnel_src,
$tunnel_dst
);
}
}
}
}
}
$tmpfname = tempnam("/tmp", "setkey");
file_put_contents($tmpfname, implode("\n", $spd_entries) . "\n");
mwexec("/sbin/setkey -f " . $tmpfname, true);
unlink($tmpfname);
}
}
/**
* setup list of hosts which will be "pinged" via cron on regular intervals
*/
function ipsec_setup_pinghosts()
{
global $config;
$a_phase1 = isset($config['ipsec']['phase1']) ? $config['ipsec']['phase1'] : [];
$a_phase2 = isset($config['ipsec']['phase2']) ? $config['ipsec']['phase2'] : [];
@unlink('/var/db/ipsecpinghosts');
if (!isset($config['ipsec']['enable'])) {
return;
}
/* resolve all local, peer addresses and setup pings */
$ipsecpinghosts = "";
/* step through each phase1 entry */
foreach ($a_phase1 as $ph1ent) {
if (isset($ph1ent['disabled']) || isset($ph1ent['mobile'])) {
continue;
}
/* step through each phase2 entry */
foreach ($a_phase2 as $ph2ent) {
if (isset($ph2ent['disabled']) || $ph1ent['ikeid'] != $ph2ent['ikeid']) {
continue;
}
/* add an ipsec pinghosts entry */
if (!empty($ph2ent['pinghost'])) {
if (!isset($iflist) || !is_array($iflist)) {
$iflist = get_configured_interface_with_descr();
}
$srcip = null;
$local_subnet = ipsec_idinfo_to_cidr($ph2ent['localid'], true, $ph2ent['mode']);
if (is_ipaddrv6($ph2ent['pinghost'])) {
foreach (array_keys($iflist) as $ifent) {
$interface_ip = get_interface_ipv6($ifent);
if (!is_ipaddrv6($interface_ip)) {
continue;
}
if (ip_in_subnet($interface_ip, $local_subnet)) {
$srcip = $interface_ip;
break;
}
}
} else {
foreach (array_keys($iflist) as $ifent) {
$interface_ip = get_interface_ip($ifent);
if (!is_ipaddrv4($interface_ip)) {
continue;
}
if ($local_subnet == "0.0.0.0/0" || ip_in_subnet($interface_ip, $local_subnet)) {
$srcip = $interface_ip;
break;
}
}
}
/* if no valid src IP was found in configured interfaces, try the vips */
if (is_null($srcip)) {
foreach (config_read_array('virtualip', 'vip') as $vip) {
if (ip_in_subnet($vip['subnet'], $local_subnet)) {
$srcip = $vip['subnet'];
break;
}
}
}
$dstip = $ph2ent['pinghost'];
if (is_ipaddrv6($dstip)) {
$family = "inet6";
} else {
$family = "inet";
}
if (is_ipaddr($srcip)) {
$ipsecpinghosts .= "{$srcip}|{$dstip}|3|||||{$family}|\n";
}
}
}
}
if (!empty($ipsecpinghosts)) {
/* get the automatic ping_hosts.sh ready */
@file_put_contents('/var/db/ipsecpinghosts', $ipsecpinghosts);
}
}
/**
* Populate /usr/local/etc/strongswan.conf
*/
function ipsec_write_strongswan_conf()
{
global $config;
$a_phase1 = isset($config['ipsec']['phase1']) ? $config['ipsec']['phase1'] : [];
$a_phase2 = isset($config['ipsec']['phase2']) ? $config['ipsec']['phase2'] : [];
$a_client = isset($config['ipsec']['client']) ? $config['ipsec']['client'] : [];
$strongswanTree = (new \OPNsense\IPsec\IPsec())->strongswanTree();
foreach ($a_phase1 as $ph1ent) {
if (isset($ph1ent['disabled'])) {
continue;
}
if ($ph1ent['mode'] ?? '' == "aggressive" && in_array($ph1ent['authentication_method'], array("pre_shared_key", "xauth_psk_server"))) {
$strongswanTree['charon']['i_dont_care_about_security_and_use_aggressive_mode_psk'] = 'yes';
break;
}
}
$strongswanTree['charon']['install_routes'] = 'no';
if (isset($a_client['enable']) && isset($a_client['net_list'])) {
$strongswanTree['charon']['cisco_unity'] = 'yes';
}
$strongswanTree['charon']['plugins'] = [];
$radius_auth_servers = null;
$disable_xauth = false;
if (isset($a_client['enable'])) {
$net_list = [];
if (isset($a_client['net_list'])) {
foreach ($a_phase1 as $ph1ent) {
if (isset($ph1ent['disabled']) || !isset($ph1ent['mobile'])) {
continue;
}
foreach ($a_phase2 as $ph2ent) {
if ($ph1ent['ikeid'] == $ph2ent['ikeid'] && !isset($ph2ent['disabled']) && !empty($ph2ent['localid'])) {
$net_list[] = ipsec_idinfo_to_cidr($ph2ent['localid'], true, $ph2ent['mode']);
}
}
}
}
$strongswanTree['charon']['plugins']['attr'] = [];
if (!empty($net_list)) {
$net_list_str = implode(",", $net_list);
$strongswanTree['charon']['plugins']['attr']['subnet'] = $net_list_str;
$strongswanTree['charon']['plugins']['attr']['split-include'] = $net_list_str;
}
$cfgservers = [];
foreach (array('dns_server1', 'dns_server2', 'dns_server3', 'dns_server4') as $dns_server) {
if (!empty($a_client[$dns_server])) {
$cfgservers[] = $a_client[$dns_server];
}
}
if (!empty($cfgservers)) {
$strongswanTree['charon']['plugins']['attr']['dns'] = implode(",", $cfgservers);
}
$cfgservers = [];
if (!empty($a_client['wins_server1'])) {
$cfgservers[] = $a_client['wins_server1'];
}
if (!empty($a_client['wins_server2'])) {
$cfgservers[] = $a_client['wins_server2'];
}
if (!empty($cfgservers)) {
$strongswanTree['charon']['plugins']['attr']['nbns'] = implode(",", $cfgservers);
}
if (!empty($a_client['dns_domain'])) {
$strongswanTree['charon']['plugins']['attr']['# Search domain and default domain'] = '';
$strongswanTree['charon']['plugins']['attr']['28674'] = $a_client['dns_domain'];
}
/*
* 28675 --> UNITY_SPLITDNS_NAME
* 25 --> INTERNAL_DNS_DOMAIN
*/
foreach (array("28675", "25") as $attr) {
if (!empty($a_client['dns_split'])) {
$strongswanTree['charon']['plugins']['attr'][$attr] = $a_client['dns_split'];
} elseif (!empty($a_client['dns_domain'])) {
$strongswanTree['charon']['plugins']['attr'][$attr] = $a_client['dns_domain'];
}
}
if (!empty($a_client['dns_split'])) {
$strongswanTree['charon']['plugins']['attr']['28675'] = $a_client['dns_split'];
}
if (!empty($a_client['login_banner'])) {
/* defang login banner, it may be multiple lines and we should not let it escape */
$strongswanTree['charon']['plugins']['attr']['28672'] = '"' . str_replace(['\\', '"'], '', $a_client['login_banner']) . '"';
}
if (isset($a_client['save_passwd'])) {
$strongswanTree['charon']['plugins']['attr']['28673'] = 1;
}
if (!empty($a_client['pfs_group'])) {
$strongswanTree['charon']['plugins']['attr']['28679'] = $a_client['pfs_group'];
}
foreach ($a_phase1 as $ph1ent) {
if (!isset($ph1ent['disabled']) && isset($ph1ent['mobile'])) {
if ($ph1ent['authentication_method'] == "eap-radius") {
$radius_auth_servers = $ph1ent['authservers'];
break; // there can only be one mobile phase1, exit loop
}
}
}
}
if (empty($radius_auth_servers) && !empty($a_client['radius_source'])) {
$radius_auth_servers = $a_client['radius_source'];
}
$mdl = new \OPNsense\IPsec\Swanctl();
if ((isset($a_client['enable']) || $mdl->isEnabled()) && !empty($radius_auth_servers)) {
$disable_xauth = true; // disable Xauth when radius is used.
$strongswanTree['charon']['plugins']['eap-radius'] = [];
$strongswanTree['charon']['plugins']['eap-radius']['servers'] = [];
$radius_server_num = 1;
$radius_accounting_enabled = false;
foreach (auth_get_authserver_list() as $auth_server) {
if (in_array($auth_server['name'], explode(',', $radius_auth_servers))) {
$server = [
'address' => $auth_server['host'],
'secret' => '"' . $auth_server['radius_secret'] . '"',
'auth_port' => $auth_server['radius_auth_port'],
];
if (!empty($auth_server['radius_acct_port'])) {
$server['acct_port'] = $auth_server['radius_acct_port'];
}
$strongswanTree['charon']['plugins']['eap-radius']['servers']['server' . $radius_server_num] = $server;
if (!empty($auth_server['radius_acct_port'])) {
$radius_accounting_enabled = true;
}
$radius_server_num += 1;
}
}
if ($radius_accounting_enabled) {
$strongswanTree['charon']['plugins']['eap-radius']['accounting'] = 'yes';
}
if ($mdl->radiusUsesGroups()) {
$strongswanTree['charon']['plugins']['eap-radius']['class_group'] = 'yes';
}
}
if ((isset($a_client['enable']) && !$disable_xauth) || (new \OPNsense\IPsec\Swanctl())->isEnabled()) {
$strongswanTree['charon']['plugins']['xauth-pam'] = [
'pam_service' => 'ipsec',
'session' => 'no',
'trim_email' => 'yes'
];
}
$strongswan = generate_strongswan_conf($strongswanTree);
$strongswan .= "\ninclude strongswan.opnsense.d/*.conf\n";
@file_put_contents("/usr/local/etc/strongswan.conf", $strongswan);
}
/**
* generate CA certificates files
*/
function ipsec_write_cas()
{
global $config;
$capath = "/usr/local/etc/swanctl/x509ca";
$cafiles = [];
foreach (isset($config['ca']) ? $config['ca'] : [] as $ca) {
$cert = base64_decode($ca['crt']);
$x509cert = openssl_x509_parse(openssl_x509_read($cert));
if (is_array($x509cert) && isset($x509cert['hash'])) {
$fname = "{$capath}/{$x509cert['hash']}.0.crt";
$cafiles[] = $fname;
file_put_contents($fname, $cert);
} else {
log_msg(sprintf('Error: Invalid certificate hash info for %s', $ca['descr']), LOG_ERR);
}
}
foreach (glob("{$capath}/*.0.crt") as $fname) {
if (!in_array($fname, $cafiles)) {
unlink($fname);
}
}
}
/**
* Write certificates
*/
function ipsec_write_certs()
{
global $config;
$a_phase1 = isset($config['ipsec']['phase1']) ? $config['ipsec']['phase1'] : [];
foreach ((new \OPNsense\IPsec\Swanctl())->getUsedCertrefs() as $certref) {
$cert = lookup_cert($certref);
if (empty($cert)) {
log_msg(sprintf('Error: Invalid certificate reference for %s', $ph1ent['name']), LOG_ERR);
continue;
}
$ph1keyfile = "/usr/local/etc/swanctl/private/{$certref}.key";
@touch($ph1keyfile);
@chmod($ph1keyfile, 0600);
file_put_contents($ph1keyfile, base64_decode($cert['prv']));
$ph1certfile = "/usr/local/etc/swanctl/x509/{$certref}.crt";
@touch($ph1certfile);
@chmod($ph1certfile, 0600);
file_put_contents($ph1certfile, base64_decode($cert['crt']));
}
foreach ($a_phase1 as $ph1ent) {
if (isset($ph1ent['disabled'])) {
continue;
}
if (!empty($ph1ent['certref'])) {
$cert = lookup_cert($ph1ent['certref']);
if (empty($cert)) {
log_msg(sprintf('Error: Invalid phase1 certificate reference for %s', $ph1ent['name']), LOG_ERR);
continue;
}
$ph1keyfile = "/usr/local/etc/swanctl/private/cert-{$ph1ent['ikeid']}.key";
@touch($ph1keyfile);
@chmod($ph1keyfile, 0600);
file_put_contents($ph1keyfile, base64_decode($cert['prv']));
$ph1certfile = "/usr/local/etc/swanctl/x509/cert-{$ph1ent['ikeid']}.crt";
@touch($ph1certfile);
@chmod($ph1certfile, 0600);
file_put_contents($ph1certfile, base64_decode($cert['crt']));
}
}
}
/**
* Generate files for key pairs (e.g. RSA)
*/
function ipsec_write_keypairs()
{
$paths = [
'publicKey' => '/usr/local/etc/swanctl/pubkey',
'privateKey' => '/usr/local/etc/swanctl/private'
];
$filenames = [];
foreach ((new \OPNsense\IPsec\IPsec())->keyPairs->keyPair->iterateItems() as $uuid => $keyPair) {
foreach ($paths as $key => $path) {
if (!empty((string)$keyPair->$key)) {
$filename = "{$path}/{$uuid}.pem";
@touch($filename);
@chmod($filename, 0600);
file_put_contents($filename, (string)$keyPair->$key);
$filenames[] = $filename;
}
}
}
foreach ($paths as $path) {
foreach (glob("{$path}/*.pem") as $filename) {
if (!in_array($filename, $filenames)) {
@unlink($filename);
}
}
}
}
/**
* build secrets structure
*/
function ipsec_write_secrets()
{
global $config;
$secrets = [];
$a_phase1 = isset($config['ipsec']['phase1']) ? $config['ipsec']['phase1'] : [];
foreach ($a_phase1 as $seq => $ph1ent) {
if (isset($ph1ent['disabled'])) {
continue;
}
if (!empty($ph1ent['pre-shared-key'])) {
$myid = isset($ph1ent['mobile']) ? ipsec_find_id($ph1ent, "local") : "";
$peerid_data = isset($ph1ent['mobile']) ? "%any" : ipsec_find_id($ph1ent, "peer");
if (!empty($peerid_data)) {
if (!empty($myid)) {
$secrets['ike-p1-' . $seq] = [
'id-0' => $myid,
'id-1' => trim($peerid_data),
'secret' => '0s' . base64_encode(trim($ph1ent['pre-shared-key']))
];
} else {
$secrets['ike-p1-' . $seq] = [
'id-0' => trim($peerid_data),
'secret' => '0s' . base64_encode(trim($ph1ent['pre-shared-key']))
];
}
}
}
}
foreach ((new \OPNsense\IPsec\IPsec())->preSharedKeys->preSharedKey->iterateItems() as $uuid => $psk) {
$keyType = $psk->keyType == 'PSK' ? 'ike' : strtolower($psk->keyType);
$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;
}
function ipsec_configure_do($verbose = false, $interface_map = null)
{
global $config;
if (!plugins_argument_map($interface_map)) {
return;
}
if (!empty($interface_map)) {
$active = false;
if (isset($config['ipsec']['phase1'])) {
foreach ($config['ipsec']['phase1'] as $phase1) {
if (!isset($phase1['disabled']) && in_array($phase1['interface'], $interface_map)) {
$active = true;
break;
}
}
}
if (!$active) {
return;
}
}
$ipsec_mdl = new \OPNsense\IPsec\IPsec();
/* configure VTI if needed */
ipsec_configure_vti();
/* setup "ping" cron job*/
ipsec_setup_pinghosts();
// Prefer older IPsec SAs (advanced setting)
if (!empty((string)$ipsec_mdl->general->preferred_oldsa)) {
set_single_sysctl('net.key.preferred_oldsa', '-30');
} else {
set_single_sysctl('net.key.preferred_oldsa', '0');
}
$ipseccfg = $config['ipsec'] ?? [];
$a_phase1 = isset($config['ipsec']['phase1']) ? $config['ipsec']['phase1'] : [];
$a_phase2 = isset($config['ipsec']['phase2']) ? $config['ipsec']['phase2'] : [];
$a_client = isset($config['ipsec']['client']) ? $config['ipsec']['client'] : [];
if (!isset($ipseccfg['enable'])) {
mwexec('/usr/local/etc/rc.d/strongswan onestop', true);
mwexec('/sbin/ifconfig enc0 down', true);
return;
}
if (get_single_sysctl('net.inet.ipsec.def_policy') == '') {
mwexec('/sbin/kldload ipsec');
}
mwexec('/sbin/ifconfig enc0 up');
service_log('Configuring IPsec VPN...', $verbose);
/* cleanup legacy ipsec.conf bits but then recreate structure to mute charon complaints */
mwexec('rm -rf /usr/local/etc/ipsec.d');
foreach (['aacerts', 'acerts', 'cacerts', 'certs', 'crls', 'ocspcerts', 'private', 'reqs'] as $dir) {
mkdir("/usr/local/etc/ipsec.d/{$dir}", 0664, true);
}
foreach (['/usr/local/etc/ipsec.conf', '/usr/local/etc/ipsec.secrets'] as $file) {
/* unlink AND copy in case the sample files are not available */
@unlink($file);
@copy("{$file}.sample", $file);
}
ipsec_write_strongswan_conf();
ipsec_write_cas();
ipsec_write_certs();
ipsec_write_keypairs();
/* begin ipsec.conf, hook mvc configuration first */
$swanctl = (new \OPNsense\IPsec\Swanctl())->getConfig();
$swanctl['secrets'] = ipsec_write_secrets();
if ((string)$ipsec_mdl->general->passthrough_networks) {
$swanctl['connections']['pass'] = [
'remote_addrs' => '127.0.0.1',
'unique' => 'replace',
'children' => [
'pass' => [
'local_ts' => (string)$ipsec_mdl->general->passthrough_networks,
'remote_ts' => (string)$ipsec_mdl->general->passthrough_networks,
'mode' => 'pass',
'start_action' => 'route'
]
]
];
}
if (count($a_phase1)) {
foreach ($a_phase1 as $ph1ent) {
if (isset($ph1ent['disabled'])) {
continue;
}
$ep = ipsec_get_phase1_src($ph1ent);
if (empty($ep)) {
continue;
}
$connection = [
'unique' => !empty($ph1ent['unique']) ? $ph1ent['unique'] : 'replace',
'aggressive' => ($ph1ent['mode'] ?? '') == 'aggressive' ? 'yes' : 'no',
'version' => ($ph1ent['iketype'] ?? '') == 'ikev2' ? 2 : 1,
'mobike' => !empty($ph1ent['mobike']) ? 'no' : 'yes',
'local_addrs' => $ep,
'local-0' => [
'id' => ipsec_find_id($ph1ent, "local")
],
'remote-0' => [
'id' => ipsec_find_id($ph1ent, "peer") ?? '%any'
],
'encap' => !empty($ph1ent['nat_traversal']) && $ph1ent['nat_traversal'] == 'force' ? 'yes' : 'no',
];
if (!isset($ph1ent['mobile'])) {
$connection['remote_addrs'] = $ph1ent['remote-gateway'];
if (!empty($ph1ent['rightallowany'])) {
$connection['remote_addrs'] .= ',0.0.0.0/0,::/0';
}
} else {
$connection['remote_addrs'] = '%any'; // default
}
if (!isset($ph1ent['reauth_enable']) && !empty($ph1ent['lifetime']) && !empty($ph1ent['margintime'])) {
// XXX: should probably move to a gui setting for reauth_time and deprecate "Disable Reauth"
$connection['reauth_time'] = ($ph1ent['lifetime'] - $ph1ent['margintime']) . ' s';
}
if (isset($ph1ent['rekey_enable'])) {
$connection['rekey_time'] = '0 s';
$connection['reauth_time'] = '0 s';
// XXX todo -> child connections.<conn>.children.<child>.life_time=0
} elseif (!empty($ph1ent['margintime']) && !empty($ph1ent['rekeyfuzz']) && !empty($ph1ent['lifetime'])) {
$connection['rekey_time'] = ($ph1ent['lifetime'] - $ph1ent['margintime']) . ' s';
$connection['over_time'] = $ph1ent['margintime'] . ' s';
$connection['rand_time'] = ($ph1ent['margintime'] * $ph1ent['rekeyfuzz']) . ' s';
}
if (!empty($ph1ent['dpd_delay']) && !empty($ph1ent['dpd_maxfail'])) {
$connection['dpd_delay'] = "{$ph1ent['dpd_delay']} s";
$dpdtimeout = $ph1ent['dpd_delay'] * ($ph1ent['dpd_maxfail'] + 1);
$connection['dpd_timeout'] = "{$dpdtimeout} s";
}
if (!empty($ph1ent['keyingtries'])) {
$connection['keyingtries'] = $ph1ent['keyingtries'] == -1 ? "0" : $ph1ent['keyingtries'];
}
if (isset($ph1ent['mobile'])) {
$pools = [];
if (!empty($a_client['pool_address'])) {
$pools[] = 'defaultv4';
if (!isset($swanctl['pools']['defaultv4'])) {
$swanctl['pools']['defaultv4'] = [
'addrs' => "{$a_client['pool_address']}/{$a_client['pool_netbits']}"
];
}
}
if (!empty($a_client['pool_address_v6'])) {
$pools[] = 'defaultv6';
if (!isset($swanctl['pools']['defaultv6'])) {
$swanctl['pools']['defaultv6'] = [
'addrs' => "{$a_client['pool_address_v6']}/{$a_client['pool_netbits_v6']}"
];
}
}
if (!empty($pools)) {
$connection['pools'] = join(',', $pools);
}
}
switch ($ph1ent['authentication_method']) {
case 'eap-tls':
$connection['local-0']['auth'] = 'eap-tls';
$connection['remote-0']['auth'] = 'eap-tls';
break;
case 'psk_eap-tls':
$connection['local-0']['auth'] = 'pubkey';
$connection['remote-0']['auth'] = 'eap-tls';
$connection['remote-0']['eap_id'] = '%any';
break;
case 'eap-mschapv2':
$connection['local-0']['auth'] = 'pubkey';
$connection['remote-0']['auth'] = 'eap-mschapv2';
$connection['remote-0']['eap_id'] = '%any';
break;
case 'rsa_eap-mschapv2':
$connection['local-0']['auth'] = 'pubkey';
$connection['remote-0']['auth'] = 'pubkey';
$connection['remote-1']['eap_id'] = '%any';
$connection['remote-1']['auth'] = 'eap-mschapv2';
break;
case 'eap-radius':
$connection['local-0']['auth'] = 'pubkey';
$connection['remote-0']['auth'] = 'eap-radius';
$connection['remote-0']['eap_id'] = '%any';
$connection['send_certreq'] = 'no';
if (!isset($connection['pools'])) {
$connection['pools'] = 'radius';
}
break;
case 'xauth_rsa_server':
$connection['local-0']['auth'] = 'pubkey';
$connection['remote-0']['auth'] = 'pubkey';
$connection['remote-1']['auth'] = 'xauth-pam';
break;
case 'xauth_psk_server':
$connection['local-0']['auth'] = 'psk';
$connection['remote-0']['auth'] = 'psk';
$connection['remote-1']['auth'] = 'xauth-pam';
break;
case 'pre_shared_key':
$connection['local-0']['auth'] = 'psk';
$connection['remote-0']['auth'] = 'psk';
break;
case 'rsasig':
case 'pubkey':
$connection['local-0']['auth'] = 'pubkey';
$connection['remote-0']['auth'] = 'pubkey';
break;
case 'hybrid_rsa_server':
$connection['local-0']['auth'] = 'pubkey';
$connection['remote-0']['auth'] = 'xauth';
break;
}
if (!empty($ph1ent['certref'])) {
$connection['local-0']['certs'] = "cert-{$ph1ent['ikeid']}.crt";
$connection['send_cert'] = 'always';
}
if (!empty($ph1ent['caref'])) {
$ca = lookup_ca($ph1ent['caref']);
if (!empty($ca)) {
$cert = base64_decode($ca['crt']);
$x509cert = openssl_x509_parse(openssl_x509_read($cert));
if (is_array($x509cert) && isset($x509cert['hash'])) {
$connection['remote-0']['cacerts'] = "{$x509cert['hash']}.0.crt";
}
}
}
if (!empty($ph1ent['local-kpref'])) {
$connection['local-0']['pubkeys'] = "{$ph1ent['local-kpref']}.pem";
}
if (!empty($ph1ent['peer-kpref'])) {
$connection['remote-0']['pubkeys'] = "{$ph1ent['peer-kpref']}.pem";
}
if (!empty($ph1ent['encryption-algorithm']['name']) && !empty($ph1ent['hash-algorithm'])) {
$list = [];
foreach (explode(',', $ph1ent['hash-algorithm']) as $halgo) {
$entry = "{$ph1ent['encryption-algorithm']['name']}";
if (isset($ph1ent['encryption-algorithm']['keylen'])) {
$entry .= "{$ph1ent['encryption-algorithm']['keylen']}";
}
$entry .= "-{$halgo}";
if (!empty($ph1ent['dhgroup'])) {
foreach (explode(',', $ph1ent['dhgroup']) as $dhgrp) {
$entryd = $entry;
$modp = ipsec_convert_to_modp($dhgrp);
if (!empty($modp)) {
$entryd .= "-{$modp}";
}
$list[] = $entryd;
}
}
}
$connection['proposals'] = implode(',', array_reverse($list));
}
// XXX: should enforce explicit choice in the gui, it's also a phase 2 property in reality
if (!empty($ph1ent['auto']) && $ph1ent['auto'] != 'add') {
$start_action = $ph1ent['auto'];
} elseif (isset($ph1ent['mobile']) || ($ph1ent['auto'] ?? '') == 'add') {
$start_action = 'none';
} elseif (!empty($config['ipsec']['auto_routes_disable'])) {
$start_action = 'start';
} else {
$start_action = 'trap';
}
$parsed_phase2 = ipsec_parse_phase2($ph1ent['ikeid']);
$base_child_conf = [
'start_action' => $start_action,
'policies' => empty($ph1ent['noinstallpolicy']) ? 'yes' : 'no',
'mode' => $parsed_phase2['type'],
'sha256_96' => isset($ph1ent['sha256_96']) ? 'yes' : 'no'
];
if (!empty($ph1ent['inactivity_timeout'])) {
$base_child_conf['inactivity'] = "{$ph1ent['inactivity_timeout']} s";
}
if (!empty($ph1ent['closeaction']) && in_array($ph1ent['closeaction'], ['hold', 'restart'])) {
$base_child_conf['close_action'] = $ph1ent['closeaction'] == 'hold' ? 'trap' : 'start';
}
if (!empty($ph1ent['dpd_delay']) && !empty($ph1ent['dpd_maxfail'])) {
if (empty($ph1ent['dpd_action']) && in_array($start_action, ['trap', 'start'])) {
$base_child_conf['dpd_action'] = 'start';
} elseif (!empty($ph1ent['dpd_action']) && $ph1ent['dpd_action'] == 'restart') {
$base_child_conf['dpd_action'] = 'start';
}
}
if (
isset($ph1ent['tunnel_isolation'])
|| (!isset($ph1ent['mobile']) && ($ph1ent['iketype'] ?? 'ikev1') == 'ikev1')
) {
$this_conn = $connection;
$this_conn['children'] = [];
for ($idx = 0; $idx < count($parsed_phase2['local_ts']); ++$idx) {
$child_id = sprintf('con%s-%03d', $ph1ent['ikeid'], $idx);
$this_conn['children'][$child_id] = $base_child_conf;
$this_conn['children'][$child_id]['local_ts'] = $parsed_phase2['local_ts'][$idx];
$this_conn['children'][$child_id]['remote_ts'] = $parsed_phase2['remote_ts'][$idx];
if (!isset($ph1ent['mobile'])) {
$this_conn['children'][$child_id]['reqid'] = $parsed_phase2['reqids'][$idx];
}
foreach (['esp_proposals', 'ah_proposals', 'life_time', 'rekey_time', 'rand_time'] as $fieldname) {
if (isset($parsed_phase2[$fieldname][$idx]) && $parsed_phase2[$fieldname][$idx] != null) {
if (is_array($parsed_phase2[$fieldname][$idx])) {
$this_conn['children'][$child_id][$fieldname] = join(
',',
$parsed_phase2[$fieldname][$idx]
);
} else {
$this_conn['children'][$child_id][$fieldname] = $parsed_phase2[$fieldname][$idx] . " s";
}
}
}
}
$swanctl['connections']["con{$ph1ent['ikeid']}"] = $this_conn;
} else {
$this_conn = $connection;
$this_connid = "con{$ph1ent['ikeid']}";
$this_conn['children'][$this_connid] = $base_child_conf;
$this_conn['children'][$this_connid]['local_ts'] = join(',', $parsed_phase2['local_ts']);
$this_conn['children'][$this_connid]['remote_ts'] = join(',', $parsed_phase2['remote_ts']);
if (!empty($parsed_phase2['reqids']) && !isset($ph1ent['mobile'])) {
$this_conn['children'][$this_connid]['reqid'] = $parsed_phase2['reqids'][0];
}
if (!empty($parsed_phase2['esp_proposals'])) {
$this_conn['children'][$this_connid]['esp_proposals'] = join(',', $parsed_phase2['esp_proposals']);
}
if (!empty($parsed_phase2['ah_proposals'])) {
$this_conn['children'][$this_connid]['ah_proposals'] = join(',', $parsed_phase2['ah_proposals']);
}
foreach (['life_time', 'rekey_time', 'rand_time'] as $fieldname) {
$values = array_diff($parsed_phase2[$fieldname], [null]);
if (!empty($values)) {
$this_conn['children'][$this_connid][$fieldname] = min($values) . " s";
}
}
$swanctl['connections'][$this_connid] = $this_conn;
}
}
}
$swanctltxt = "# This file is automatically generated. Do not edit\n";
$swanctltxt .= generate_strongswan_conf($swanctl);
$swanctltxt .= "# Include config snippets\n";
$swanctltxt .= "include conf.d/*.conf\n";
file_put_contents("/usr/local/etc/swanctl/swanctl.conf", $swanctltxt);
/* mange process */
if (isvalidpid('/var/run/charon.pid')) {
/* Update configuration changes */
mwexec_bg('/usr/local/etc/rc.d/strongswan onereload', false);
} else {
mwexec_bg("/usr/local/etc/rc.d/strongswan onestart", false);
}
/* load manually defined SPD entries */
ipsec_configure_spd();
service_log("done.\n", $verbose);
/* reload routes on all attached VTI devices */
plugins_configure('route_reload', $verbose, [array_keys(array_merge(ipsec_get_configured_vtis(), (new \OPNsense\IPsec\Swanctl())->getVtiDevices()))]);
}
function generate_strongswan_conf(array $tree, $level = 0): string
{
$output = "";
foreach ($tree as $key => $value) {
$output .= str_repeat(' ', $level) . $key;
if (strpos($key, '#') === 0) {
$output .= "\n";
} elseif (is_array($value)) {
$output .= " {\n";
$output .= generate_strongswan_conf($value, $level + 1);
$output .= str_repeat(' ', $level) . "}\n";
} else {
$output .= " = " . $value . "\n";
}
}
return $output;
}
function ipsec_get_configured_vtis()
{
global $config;
$a_phase1 = isset($config['ipsec']['phase1']) ? $config['ipsec']['phase1'] : [];
$a_phase2 = isset($config['ipsec']['phase2']) ? $config['ipsec']['phase2'] : [];
$configured_intf = [];
foreach ($a_phase1 as $ph1ent) {
if (empty($ph1ent['disabled'])) {
$phase2items = [];
$phase2reqids = [];
foreach ($a_phase2 as $ph2ent) {
if (
isset($ph2ent['mode']) && $ph2ent['mode'] == 'route-based' &&
empty($ph2ent['disabled']) && $ph1ent['ikeid'] == $ph2ent['ikeid']
) {
$phase2items[] = $ph2ent;
if (!empty($ph2ent['reqid'])) {
$phase2reqids[] = $ph2ent['reqid'];
}
}
}
foreach ($phase2items as $idx => $phase2) {
if (empty($phase2['reqid'])) {
continue;
} elseif ((!isset($ph1ent['mobile']) && $ph1ent['iketype'] == 'ikev1') || isset($ph1ent['tunnel_isolation'])) {
// isolated tunnels, every tunnel it's own reqid
$reqid = $phase2['reqid'];
$descr = empty($phase2['descr']) ? $ph1ent['descr'] : $phase2['descr'];
} else {
// use smallest reqid within tunnel
$reqid = min($phase2reqids);
$descr = $ph1ent['descr'] ?? '';
}
$intfnm = sprintf("ipsec%s", $reqid);
if (empty($configured_intf[$intfnm])) {
$configured_intf[$intfnm] = ['reqid' => $reqid];
$configured_intf[$intfnm]['local'] = ipsec_get_phase1_src($ph1ent);
$configured_intf[$intfnm]['remote'] = $ph1ent['remote-gateway'];
$configured_intf[$intfnm]['descr'] = $descr;
$configured_intf[$intfnm]['networks'] = [];
}
$inet = is_ipaddrv6($phase2['tunnel_local']) ? 'inet6' : 'inet';
$configured_intf[$intfnm]['networks'][] = [
'inet' => $inet,
'mask' => find_smallest_cidr([$phase2['tunnel_local'], $phase2['tunnel_remote']], $inet),
'tunnel_local' => $phase2['tunnel_local'],
'tunnel_remote' => $phase2['tunnel_remote']
];
}
}
}
return $configured_intf;
}
/**
* Configure required Virtual Terminal Interfaces (synchronizes configuration with local interfaces named ipsec%)
*/
function ipsec_configure_vti($verbose = false, $device = null)
{
// query planned and configured interfaces
$configured_intf = array_merge(ipsec_get_configured_vtis(), (new \OPNsense\IPsec\Swanctl())->getVtiDevices());
$current_interfaces = [];
foreach (legacy_interfaces_details() as $intf => $intf_details) {
if (strpos($intf, 'ipsec') === 0) {
$current_interfaces[$intf] = $intf_details;
}
}
service_log(sprintf('Creating IPsec VTI instance%s...', empty($device) ? 's' : " {$device}"), $verbose);
// drop changed or not existing interfaces and tunnel endpoints
foreach ($current_interfaces as $intf => $intf_details) {
if ($device !== null && $device != $intf) {
continue;
}
if (empty($configured_intf[$intf])) {
log_msg(sprintf("destroy interface %s", $intf), LOG_DEBUG);
legacy_interface_destroy($intf);
unset($current_interfaces[$intf]);
} else {
foreach (['ipv4', 'ipv6'] as $proto) {
foreach ($intf_details[$proto] as $addr) {
if (!empty($addr['endpoint'])) {
$isfound = false;
foreach ($configured_intf[$intf]['networks'] as $network) {
if (
$network['tunnel_local'] == $addr['ipaddr']
&& $network['tunnel_remote'] == $addr['endpoint']
) {
$isfound = true;
break;
}
}
if (!$isfound) {
log_msg(sprintf(
"remove tunnel %s %s from interface %s",
$addr['ipaddr'],
$addr['endpoint'],
$intf
), LOG_DEBUG);
mwexecf('/sbin/ifconfig %s %s %s delete', [
$intf, $proto == 'ipv6' ? 'inet6' : 'inet', $addr['ipaddr'], $addr['endpoint']
]);
}
}
}
}
}
}
// configure new interfaces and tunnels
foreach ($configured_intf as $intf => $intf_details) {
if ($device !== null && $device != $intf) {
continue;
}
// create required interfaces
if (empty($current_interfaces[$intf])) {
// prevent ipsec vti interface to hit 32768 limit (create numbered, rename and attach afterwards)
if (legacy_interface_create('ipsec', $intf) === null) {
break;
}
}
// [re]initialise if_ipsec
mwexecf('/sbin/ifconfig %s reqid %s', [$intf, $intf_details['reqid']]);
if (!empty($intf_details['local']) && !empty($intf_details['remote'])) {
mwexecf('/sbin/ifconfig %s %s tunnel %s %s up', [
$intf,
is_ipaddrv6($intf_details['local']) ? 'inet6' : 'inet',
$intf_details['local'],
$intf_details['remote']
]);
}
// create new tunnel endpoints
foreach ($intf_details['networks'] as $endpoint) {
if (!empty($current_interfaces[$intf])) {
$already_configured = $current_interfaces[$intf][$endpoint['inet'] == 'inet6' ? 'ipv6' : 'ipv4'];
} else {
$already_configured = [];
}
$isfound = false;
foreach ($already_configured as $addr) {
if (!empty($addr['endpoint'])) {
if (
$endpoint['tunnel_local'] == $addr['ipaddr']
&& $endpoint['tunnel_remote'] == $addr['endpoint']
) {
$isfound = true;
}
}
}
if (!$isfound) {
if ($endpoint['inet'] == 'inet') {
mwexecf('/sbin/ifconfig %s %s %s %s', [
$intf, $endpoint['inet'], sprintf("%s/%s", $endpoint['tunnel_local'], $endpoint['mask']),
$endpoint['tunnel_remote']
]);
} else {
// XXX: don't specify a tunnel endpoint for ipv6, although this looks like an illogical
// construction, a netmask seems to be a requirement for some ipv6 consumers (frr)
mwexecf('/sbin/ifconfig %s %s %s', [
$intf, $endpoint['inet'], sprintf("%s/%s", $endpoint['tunnel_local'], $endpoint['mask'])
]);
}
}
}
}
service_log("done.\n", $verbose);
}
function ipsec_configure_device($device)
{
ipsec_configure_vti(false, $device);
}