summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Poessinger <christian@poessinger.com>2021-07-20 20:58:05 +0200
committerChristian Poessinger <christian@poessinger.com>2021-07-20 20:59:14 +0200
commit69614d7d501811164010a83441ea807716903cf1 (patch)
tree40c739f085f19906dee341ca1b03ea3a944801ba
parent4d55afded46a07c761a724989e0e66fe88d705c7 (diff)
downloadvyos-1x-69614d7d501811164010a83441ea807716903cf1.tar.gz
vyos-1x-69614d7d501811164010a83441ea807716903cf1.zip
ipsec: T1210: add op-mode command for macOS and iOS profile generation
generate ipsec mac-ios-profile <connection> remote <ip|fqdn> will generate a matching IPSec profile which can be loaded on an iOS device.
-rw-r--r--data/templates/ipsec/ios_profile.tmpl110
-rw-r--r--op-mode-definitions/generate-ipsec-profile.xml.in76
-rw-r--r--python/vyos/template.py6
-rwxr-xr-xsrc/op_mode/ikev2_profile_generator.py79
4 files changed, 271 insertions, 0 deletions
diff --git a/data/templates/ipsec/ios_profile.tmpl b/data/templates/ipsec/ios_profile.tmpl
new file mode 100644
index 000000000..508f801d2
--- /dev/null
+++ b/data/templates/ipsec/ios_profile.tmpl
@@ -0,0 +1,110 @@
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <!-- Set the name to whatever you like, it is used in the profile list on the device -->
+ <key>PayloadDisplayName</key>
+ <string>{{ profile_name }}</string>
+ <!-- This is a reverse-DNS style unique identifier used to detect duplicate profiles -->
+ <key>PayloadIdentifier</key>
+ <string>{{ rfqdn }}</string>
+ <!-- A globally unique identifier, use uuidgen on Linux/Mac OS X to generate it -->
+ <key>PayloadUUID</key>
+ <string>{{ 'random' | get_uuid }}</string>
+ <key>PayloadType</key>
+ <string>Configuration</string>
+ <key>PayloadVersion</key>
+ <integer>1</integer>
+ <key>PayloadContent</key>
+ <array>
+ <!-- It is possible to add multiple VPN payloads with different identifiers/UUIDs and names -->
+ <dict>
+ <!-- This is an extension of the identifier given above -->
+ <key>PayloadIdentifier</key>
+ <string>{{ rfqdn }}.conf1</string>
+ <!-- A globally unique identifier for this payload -->
+ <key>PayloadUUID</key>
+ <string>{{ 'random' | get_uuid }}</string>
+ <key>PayloadType</key>
+ <string>com.apple.vpn.managed</string>
+ <key>PayloadVersion</key>
+ <integer>1</integer>
+ <!-- This is the name of the VPN connection as seen in the VPN application later -->
+ <key>UserDefinedName</key>
+ <string>{{ vpn_name }}</string>
+ <key>VPNType</key>
+ <string>IKEv2</string>
+ <key>IKEv2</key>
+ <dict>
+ <!-- Hostname or IP address of the VPN server -->
+ <key>RemoteAddress</key>
+ <string>{{ remote }}</string>
+ <!-- Remote identity, can be a FQDN, a userFQDN, an IP or (theoretically) a certificate's subject DN. Can't be empty.
+ IMPORTANT: DNs are currently not handled correctly, they are always sent as identities of type FQDN -->
+ <key>RemoteIdentifier</key>
+ <string>{{ authentication.id if authentication.id is defined else 'fooo' }}</string>
+ <!-- Local IKE identity, same restrictions as above. If it is empty the client's IP address will be used -->
+ <key>LocalIdentifier</key>
+ <string></string>
+ <!-- Optional, if it matches the CN of the root CA certificate (not the full subject DN) a certificate request will be sent
+ NOTE: If this is not configured make sure to configure leftsendcert=always on the server, otherwise it won't send its certificate -->
+ <key>ServerCertificateIssuerCommonName</key>
+ <string>{{ ca_cn }}</string>
+ <!-- Optional, the CN or one of the subjectAltNames of the server certificate to verify it, if not set RemoteIdentifier will be used -->
+ <key>ServerCertificateCommonName</key>
+ <string>{{ cert_cn }}</string>
+ <!-- The server is authenticated using a certificate -->
+ <key>AuthenticationMethod</key>
+ <string>Certificate</string>
+ <!-- The client uses EAP to authenticate -->
+ <key>ExtendedAuthEnabled</key>
+ <integer>1</integer>
+{% if ike_proposal is defined and ike_proposal is not none %}
+ <!-- The next two dictionaries are optional (as are the keys in them), but it is recommended to specify them as the default is to use 3DES.
+ IMPORTANT: Because only one proposal is sent (even if nothing is configured here) it must match the server configuration -->
+ <key>IKESecurityAssociationParameters</key>
+{% for ike, ike_config in ike_proposal.items() %}
+ <dict>
+ <!-- @see https://developer.apple.com/documentation/networkextension/nevpnikev2encryptionalgorithm -->
+ <key>EncryptionAlgorithm</key>
+ <string>{{ ike_config.encryption | upper }}</string>
+ <!-- @see https://developer.apple.com/documentation/networkextension/nevpnikev2integrityalgorithm -->
+ <key>IntegrityAlgorithm</key>
+ <string>{{ ike_config.hash | upper }}</string>
+ <!-- @see https://developer.apple.com/documentation/networkextension/nevpnikev2diffiehellmangroup -->
+ <key>DiffieHellmanGroup</key>
+ <integer>{{ ike_config.dh_group | upper }}
+ </dict>
+{% endfor %}
+{% endif %}
+{% if esp_proposal is defined and esp_proposal is not none %}
+ <key>ChildSecurityAssociationParameters</key>
+{% for esp, esp_config in esp_proposal.items() %}
+ <dict>
+ <key>EncryptionAlgorithm</key>
+ <string>{{ esp_config.encryption | upper }}</string>
+ <key>IntegrityAlgorithm</key>
+ <string>{{ esp_config.hash | upper }}</string>
+ </dict>
+{% endfor %}
+{% endif %}
+ </dict>
+ </dict>
+ <!-- This payload is optional but it provides an easy way to install the CA certificate together with the configuration -->
+ <dict>
+ <key>PayloadIdentifier</key>
+ <string>org.example.ca</string>
+ <key>PayloadUUID</key>
+ <string>{{ 'random' | get_uuid }}</string>
+ <key>PayloadType</key>
+ <string>com.apple.security.root</string>
+ <key>PayloadVersion</key>
+ <integer>1</integer>
+ <!-- This is the Base64 (PEM) encoded CA certificate -->
+ <key>PayloadContent</key>
+ <data>
+ {{ ca_cert }}
+ </data>
+ </dict>
+ </array>
+</dict>
+</plist>
diff --git a/op-mode-definitions/generate-ipsec-profile.xml.in b/op-mode-definitions/generate-ipsec-profile.xml.in
new file mode 100644
index 000000000..d1e5efd20
--- /dev/null
+++ b/op-mode-definitions/generate-ipsec-profile.xml.in
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="generate">
+ <children>
+ <node name="ipsec">
+ <properties>
+ <help>Generate IPsec related configurations</help>
+ </properties>
+ <children>
+ <tagNode name="mac-ios-profile">
+ <properties>
+ <help>Generate Apple iOS profile from IPsec connection profile</help>
+ <completionHelp>
+ <path>vpn ipsec remote-access connection</path>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="remote">
+ <properties>
+ <help>Remote address where the client will connect to</help>
+ <completionHelp>
+ <list>&lt;fqdn&gt;</list>
+ <script>${vyos_completion_dir}/list_local_ips.sh --both</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --connection "$4" --remote "$6"</command>
+ <children>
+ <tagNode name="name">
+ <properties>
+ <help>Connection name as seen in the VPN application</help>
+ <completionHelp>
+ <list>&lt;name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --connection "$4" --remote "$6" --name "$8"</command>
+ <children>
+ <tagNode name="profile">
+ <properties>
+ <help>Profile name as seen under system profiles</help>
+ <completionHelp>
+ <list>&lt;name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --connection "$4" --remote "$6" --name "$8" --profile "${10}"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="profile">
+ <properties>
+ <help>Profile name as seen under system profiles</help>
+ <completionHelp>
+ <list>&lt;name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --connection "$4" --remote "$6" --profile "$8"</command>
+ <children>
+ <tagNode name="name">
+ <properties>
+ <help>Connection name as seen in the VPN application</help>
+ <completionHelp>
+ <list>&lt;name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --connection "$4" --remote "$6" --profile "$8" --name "${10}"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/python/vyos/template.py b/python/vyos/template.py
index f03fd7ee7..0d2bd39e7 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -433,3 +433,9 @@ def get_esp_ike_cipher(group_config):
ciphers.append(tmp)
return ciphers
+
+@register_filter('get_uuid')
+def get_uuid(interface):
+ """ Get interface IP addresses"""
+ from uuid import uuid1
+ return uuid1()
diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py
new file mode 100755
index 000000000..4ff37341c
--- /dev/null
+++ b/src/op_mode/ikev2_profile_generator.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import argparse
+
+from jinja2 import Template
+from sys import exit
+from socket import getfqdn
+
+from vyos.config import Config
+from vyos.template import render_to_string
+from cryptography.x509.oid import NameOID
+from vyos.pki import load_certificate
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--connection", action="store", help="IPsec IKEv2 remote-access connection name from CLI", required=True)
+parser.add_argument("--remote", action="store", help="VPN connection remote-address where the client will connect to", required=True)
+parser.add_argument("--profile", action="store", help="IKEv2 profile name used in the profile list on the device")
+parser.add_argument("--name", action="store", help="VPN connection name as seen in the VPN application later")
+args = parser.parse_args()
+
+ipsec_base = ['vpn', 'ipsec']
+config_base = ipsec_base + ['remote-access', 'connection']
+pki_base = ['pki']
+conf = Config()
+if not conf.exists(config_base):
+ exit('IPSec remote-access is not configured!')
+
+profile_name = 'VyOS IKEv2 Profile'
+if args.profile:
+ profile_name = args.profile
+
+vpn_name = 'VyOS IKEv2 Profile'
+if args.name:
+ vpn_name = args.name
+
+conn_base = config_base + [args.connection]
+if not conf.exists(conn_base):
+ exit(f'IPSec remote-access connection "{args.connection}" does not exist!')
+
+data = conf.get_config_dict(conn_base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+data['profile_name'] = profile_name
+data['vpn_name'] = vpn_name
+data['remote'] = args.remote
+# This is a reverse-DNS style unique identifier used to detect duplicate profiles
+tmp = getfqdn().split('.')
+tmp = reversed(tmp)
+data['rfqdn'] = '.'.join(tmp)
+
+pki = conf.get_config_dict(pki_base, get_first_key=True)
+ca_name = data['authentication']['x509']['ca_certificate']
+cert_name = data['authentication']['x509']['certificate']
+
+ca_cert = load_certificate(pki['ca'][ca_name]['certificate'])
+cert = load_certificate(pki['certificate'][cert_name]['certificate'])
+
+data['ca_cn'] = ca_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
+data['cert_cn'] = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
+data['ca_cert'] = conf.return_value(pki_base + ['ca', ca_name, 'certificate'])
+
+data['esp_proposal'] = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group'], 'proposal'], key_mangling=('-', '_'), get_first_key=True)
+data['ike_proposal'] = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group'], 'proposal'], key_mangling=('-', '_'), get_first_key=True)
+
+print(render_to_string('ipsec/ios_profile.tmpl', data))