From 65765fe95a34d81ad4a3aedb035936bbaf6a3f0e Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 29 Jul 2021 18:47:42 +0200
Subject: ipsec: T1210: add op-mode command to print Windows connection profile

---
 data/templates/ipsec/windows_profile.tmpl         |   4 +
 op-mode-definitions/generate-ipsec-profile.xml.in | 111 +++++++++---
 src/op_mode/ikev2_profile_generator.py            | 201 ++++++++++++++--------
 3 files changed, 222 insertions(+), 94 deletions(-)
 create mode 100644 data/templates/ipsec/windows_profile.tmpl

diff --git a/data/templates/ipsec/windows_profile.tmpl b/data/templates/ipsec/windows_profile.tmpl
new file mode 100644
index 000000000..8c26944be
--- /dev/null
+++ b/data/templates/ipsec/windows_profile.tmpl
@@ -0,0 +1,4 @@
+Remove-VpnConnection -Name "{{ vpn_name }}" -Force -PassThru
+
+Add-VpnConnection -Name "{{ vpn_name }}" -ServerAddress "{{ remote }}" -TunnelType "Ikev2"
+Set-VpnConnectionIPsecConfiguration -ConnectionName "{{ vpn_name }}" -AuthenticationTransformConstants {{ ike_encryption.encryption }} -CipherTransformConstants {{ ike_encryption.encryption }} -EncryptionMethod {{ esp_encryption.encryption }} -IntegrityCheckMethod {{ esp_encryption.hash }} -PfsGroup None -DHGroup "Group{{ ike_encryption.dh_group }}" -PassThru -Force
diff --git a/op-mode-definitions/generate-ipsec-profile.xml.in b/op-mode-definitions/generate-ipsec-profile.xml.in
index d1e5efd20..be9227971 100644
--- a/op-mode-definitions/generate-ipsec-profile.xml.in
+++ b/op-mode-definitions/generate-ipsec-profile.xml.in
@@ -7,33 +7,49 @@
           <help>Generate IPsec related configurations</help>
         </properties>
         <children>
-          <tagNode name="mac-ios-profile">
+          <node name="profile">
             <properties>
-              <help>Generate Apple iOS profile from IPsec connection profile</help>
-              <completionHelp>
-                <path>vpn ipsec remote-access connection</path>
-              </completionHelp>
+              <help>Generate IKEv2 IPSec remote-access VPN profiles</help>
             </properties>
             <children>
-              <tagNode name="remote">
+              <tagNode name="ios-remote-access">
                 <properties>
-                  <help>Remote address where the client will connect to</help>
+                  <help>Generate iOS profile for specified remote-access connection name</help>
                   <completionHelp>
-                    <list>&lt;fqdn&gt;</list>
-                    <script>${vyos_completion_dir}/list_local_ips.sh --both</script>
+                    <path>vpn ipsec remote-access connection</path>
                   </completionHelp>
                 </properties>
-                <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --connection "$4" --remote "$6"</command>
                 <children>
-                  <tagNode name="name">
+                  <tagNode name="remote">
                     <properties>
-                      <help>Connection name as seen in the VPN application</help>
+                      <help>Remote address where the client will connect to</help>
                       <completionHelp>
-                        <list>&lt;name&gt;</list>
+                        <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" --name "$8"</command>
+                    <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os ios --connection "$5" --remote "$7"</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 --os ios --connection "$5" --remote "$7" --name "$9"</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 --os ios --connection "$5" --remote "$7" --name "$9" --profile "${11}"</command>
+                          </tagNode>
+                        </children>
+                      </tagNode>
                       <tagNode name="profile">
                         <properties>
                           <help>Profile name as seen under system profiles</help>
@@ -41,18 +57,40 @@
                             <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>
+                        <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os ios --connection "$5" --remote "$7" --profile "$9"</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 --os ios --connection "$5" --remote "$7" --profile "$9" --name "${11}"</command>
+                          </tagNode>
+                        </children>
                       </tagNode>
                     </children>
                   </tagNode>
-                  <tagNode name="profile">
+                </children>
+              </tagNode>
+              <tagNode name="windows-remote-access">
+                <properties>
+                  <help>Generate iOS profile for specified remote-access connection name</help>
+                  <completionHelp>
+                    <path>vpn ipsec remote-access connection</path>
+                  </completionHelp>
+                </properties>
+                <children>
+                  <tagNode name="remote">
                     <properties>
-                      <help>Profile name as seen under system profiles</help>
+                      <help>Remote address where the client will connect to</help>
                       <completionHelp>
-                        <list>&lt;name&gt;</list>
+                        <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" --profile "$8"</command>
+                    <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os windows --connection "$5" --remote "$7"</command>
                     <children>
                       <tagNode name="name">
                         <properties>
@@ -61,14 +99,45 @@
                             <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>
+                        <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os windows --connection "$5" --remote "$7" --name "$9"</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 --os windows --connection "$5" --remote "$7" --name "$9" --profile "${11}"</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 --os windows --connection "$5" --remote "$7" --profile "$9"</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 --os windows --connection "$5" --remote "$7" --profile "$9" --name "${11}"</command>
+                          </tagNode>
+                        </children>
                       </tagNode>
                     </children>
                   </tagNode>
                 </children>
               </tagNode>
             </children>
-          </tagNode>
+          </node>
         </children>
       </node>
     </children>
diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py
index ce93ec057..d45525431 100755
--- a/src/op_mode/ikev2_profile_generator.py
+++ b/src/op_mode/ikev2_profile_generator.py
@@ -26,54 +26,6 @@ from vyos.pki import load_certificate
 from vyos.template import render_to_string
 from vyos.util import ask_input
 
-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'])
-
 # Apple profiles only support one IKE/ESP encryption cipher and hash, whereas
 # VyOS comes with a multitude of different proposals for a connection.
 #
@@ -99,6 +51,25 @@ vyos2apple_cipher = {
     'chacha20poly1305' : 'ChaCha20Poly1305',
 }
 
+# Windows supports IKE-SA encryption algorithms:
+# - DES3
+# - AES128
+# - AES192
+# - AES256
+# - GCMAES128
+# - GCMAES192
+# - GCMAES256
+#
+vyos2windows_cipher = {
+    '3des'  : 'DES3',
+    'aes128' : 'AES128',
+    'aes192' : 'AES192',
+    'aes256' : 'AES256',
+    'aes128gcm128' : 'GCMAES128',
+    'aes192gcm128' : 'GCMAES192',
+    'aes256gcm128' : 'GCMAES256',
+}
+
 # IOS supports IKE-SA integrity algorithms:
 # - SHA1-96
 # - SHA1-160
@@ -114,27 +85,102 @@ vyos2apple_integrity = {
     'sha512'   : 'SHA2-512',
 }
 
+# Windows supports IKE-SA integrity algorithms:
+# - SHA1-96
+# - SHA1-160
+# - SHA2-256
+# - SHA2-384
+# - SHA2-512
+#
+vyos2windows_integrity = {
+    'sha1'       : 'SHA196',
+    'sha256'     : 'SHA256',
+    'aes128gmac' : 'GCMAES128',
+    'aes192gmac' : 'GCMAES192',
+    'aes256gmac' : 'GCMAES256',
+}
+
 # IOS 14.2 and later do no support dh-group 1,2 and 5. Supported DH groups would
 # be: 14, 15, 16, 17, 18, 19, 20, 21, 31
-supported_dh_groups = ['14', '15', '16', '17', '18', '19', '20', '21', '31']
+ios_supported_dh_groups = ['14', '15', '16', '17', '18', '19', '20', '21', '31']
+# Windows 10 only allows a limited set of DH groups
+windows_supported_dh_groups = ['1', '2', '14', '24']
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--os', const='all', nargs='?', choices=['ios', 'windows'], help='Operating system used for config generation', required=True)
+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 VPN'
+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'])
 
 esp_proposals = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group'], 'proposal'],
                                      key_mangling=('-', '_'), get_first_key=True)
 ike_proposal = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group'], 'proposal'],
                                     key_mangling=('-', '_'), get_first_key=True)
 
-# Create a dictionary containing Apple conform IKE settings
+
+# This script works only for Apple iOS/iPadOS and Windows. Both operating systems
+# have different limitations thus we load the limitations based on the operating
+# system used.
+
+vyos2client_cipher = vyos2apple_cipher if args.os == 'ios' else vyos2windows_cipher;
+vyos2client_integrity = vyos2apple_integrity if args.os == 'ios' else vyos2windows_integrity;
+supported_dh_groups = ios_supported_dh_groups if args.os == 'ios' else windows_supported_dh_groups;
+
+# Create a dictionary containing client conform IKE settings
 ike = {}
 count = 1
 for _, proposal in ike_proposal.items():
     if {'dh_group', 'encryption', 'hash'} <= set(proposal):
-        if (proposal['encryption'] in set(vyos2apple_cipher) and
-            proposal['hash'] in set(vyos2apple_integrity) and
+        if (proposal['encryption'] in set(vyos2client_cipher) and
+            proposal['hash'] in set(vyos2client_integrity) and
             proposal['dh_group'] in set(supported_dh_groups)):
 
             # We 're-code' from the VyOS IPSec proposals to the Apple naming scheme
-            proposal['encryption'] = vyos2apple_cipher[ proposal['encryption'] ]
-            proposal['hash'] = vyos2apple_integrity[ proposal['hash'] ]
+            proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
+            proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
 
             ike.update( { str(count) : proposal } )
             count += 1
@@ -144,32 +190,41 @@ esp = {}
 count = 1
 for _, proposal in esp_proposals.items():
     if {'encryption', 'hash'} <= set(proposal):
-        if proposal['encryption'] in set(vyos2apple_cipher) and proposal['hash'] in set(vyos2apple_integrity):
+        if proposal['encryption'] in set(vyos2client_cipher) and proposal['hash'] in set(vyos2client_integrity):
             # We 're-code' from the VyOS IPSec proposals to the Apple naming scheme
-            proposal['encryption'] = vyos2apple_cipher[ proposal['encryption'] ]
-            proposal['hash'] = vyos2apple_integrity[ proposal['hash'] ]
+            proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
+            proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
 
             esp.update( { str(count) : proposal } )
             count += 1
 try:
-    # Propare the input questions for the user
-    tmp = '\n'
-    for number, options in ike.items():
-        tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}, DH group {options["dh_group"]}\n'
-    tmp += '\nSelect one of the above IKE groups: '
-    data['ike_encryption'] = ike[ ask_input(tmp, valid_responses=list(ike)) ]
-
-    tmp = '\n'
-    for number, options in esp.items():
-        tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}\n'
-    tmp += '\nSelect one of the above ESP groups: '
-    data['esp_encryption'] = esp[ ask_input(tmp, valid_responses=list(esp)) ]
-
+    if len(ike) > 1:
+        # Propare the input questions for the user
+        tmp = '\n'
+        for number, options in ike.items():
+            tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}, DH group {options["dh_group"]}\n'
+        tmp += '\nSelect one of the above IKE groups: '
+        data['ike_encryption'] = ike[ ask_input(tmp, valid_responses=list(ike)) ]
+    else:
+        data['ike_encryption'] = ike['1']
+
+    if len(esp) > 1:
+        tmp = '\n'
+        for number, options in esp.items():
+            tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}\n'
+        tmp += '\nSelect one of the above ESP groups: '
+        data['esp_encryption'] = esp[ ask_input(tmp, valid_responses=list(esp)) ]
+    else:
+        data['esp_encryption'] = esp['1']
 
 except KeyboardInterrupt:
     exit("Interrupted")
 
 print('\n\n==== <snip> ====')
-print(render_to_string('ipsec/ios_profile.tmpl', data))
-print('==== </snip> ====\n')
-print('Save the XML from above to a new file named "vyos.mobileconfig" and E-Mail it to your phone.')
+if args.os == 'ios':
+    print(render_to_string('ipsec/ios_profile.tmpl', data))
+    print('==== </snip> ====\n')
+    print('Save the XML from above to a new file named "vyos.mobileconfig" and E-Mail it to your phone.')
+elif args.os == 'windows':
+    print(render_to_string('ipsec/windows_profile.tmpl', data))
+    print('==== </snip> ====\n')
-- 
cgit v1.2.3