diff options
author | Christian Poessinger <christian@poessinger.com> | 2021-07-20 20:58:05 +0200 |
---|---|---|
committer | Christian Poessinger <christian@poessinger.com> | 2021-07-20 20:59:14 +0200 |
commit | 69614d7d501811164010a83441ea807716903cf1 (patch) | |
tree | 40c739f085f19906dee341ca1b03ea3a944801ba | |
parent | 4d55afded46a07c761a724989e0e66fe88d705c7 (diff) | |
download | vyos-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.tmpl | 110 | ||||
-rw-r--r-- | op-mode-definitions/generate-ipsec-profile.xml.in | 76 | ||||
-rw-r--r-- | python/vyos/template.py | 6 | ||||
-rwxr-xr-x | src/op_mode/ikev2_profile_generator.py | 79 |
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><fqdn></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><name></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><name></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><name></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><name></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)) |