summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/vrf.py17
-rwxr-xr-xsrc/op_mode/generate_ovpn_client_file.py113
-rwxr-xr-xsrc/op_mode/ikev2_profile_generator.py85
3 files changed, 154 insertions, 61 deletions
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 184725573..72b178c89 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sys import exit
+from jmespath import search
from json import loads
from vyos.config import Config
@@ -70,6 +71,14 @@ def has_rule(af : str, priority : int, table : str=None):
return True
return False
+def is_nft_vrf_zone_rule_setup() -> bool:
+ """
+ Check if an nftables connection tracking rule already exists
+ """
+ tmp = loads(cmd('sudo nft -j list table inet vrf_zones'))
+ num_rules = len(search("nftables[].rule[].chain", tmp))
+ return bool(num_rules)
+
def vrf_interfaces(c, match):
matched = []
old_level = c.get_level()
@@ -264,6 +273,7 @@ def apply(vrf):
if not has_rule(afi, 2000, 'l3mdev'):
call(f'ip {afi} rule add pref 2000 l3mdev unreachable')
+ nft_vrf_zone_rule_setup = False
for name, config in vrf['name'].items():
table = config['table']
if not interface_exists(name):
@@ -302,7 +312,12 @@ def apply(vrf):
nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}'
cmd(f'nft {nft_add_element}')
- if vrf['conntrack']:
+ # Only call into nftables as long as there is nothing setup to avoid wasting
+ # CPU time and thus lenghten the commit process
+ if not nft_vrf_zone_rule_setup:
+ nft_vrf_zone_rule_setup = is_nft_vrf_zone_rule_setup()
+ # Install nftables conntrack rules only once
+ if vrf['conntrack'] and not nft_vrf_zone_rule_setup:
for chain, rule in nftables_rules.items():
cmd(f'nft add rule inet vrf_zones {chain} {rule}')
diff --git a/src/op_mode/generate_ovpn_client_file.py b/src/op_mode/generate_ovpn_client_file.py
index 2d96fe217..974f7d9b6 100755
--- a/src/op_mode/generate_ovpn_client_file.py
+++ b/src/op_mode/generate_ovpn_client_file.py
@@ -19,42 +19,53 @@ import argparse
from jinja2 import Template
from textwrap import fill
-from vyos.configquery import ConfigTreeQuery
+from vyos.config import Config
from vyos.ifconfig import Section
client_config = """
client
nobind
-remote {{ remote_host }} {{ port }}
+remote {{ local_host if local_host else 'x.x.x.x' }} {{ port }}
remote-cert-tls server
-proto {{ 'tcp-client' if protocol == 'tcp-active' else 'udp' }}
-dev {{ device }}
-dev-type {{ device }}
+proto {{ 'tcp-client' if protocol == 'tcp-passive' else 'udp' }}
+dev {{ device_type }}
+dev-type {{ device_type }}
persist-key
persist-tun
verb 3
# Encryption options
+{# Define the encryption map #}
+{% set encryption_map = {
+ 'des': 'DES-CBC',
+ '3des': 'DES-EDE3-CBC',
+ 'bf128': 'BF-CBC',
+ 'bf256': 'BF-CBC',
+ 'aes128gcm': 'AES-128-GCM',
+ 'aes128': 'AES-128-CBC',
+ 'aes192gcm': 'AES-192-GCM',
+ 'aes192': 'AES-192-CBC',
+ 'aes256gcm': 'AES-256-GCM',
+ 'aes256': 'AES-256-CBC'
+} %}
+
{% if encryption is defined and encryption is not none %}
-{% if encryption.cipher is defined and encryption.cipher is not none %}
-cipher {{ encryption.cipher }}
-{% if encryption.cipher == 'bf128' %}
-keysize 128
-{% elif encryption.cipher == 'bf256' %}
-keysize 256
+{% if encryption.ncp_ciphers is defined and encryption.ncp_ciphers is not none %}
+cipher {% for algo in encryption.ncp_ciphers %}
+{{ encryption_map[algo] if algo in encryption_map.keys() else algo }}{% if not loop.last %}:{% endif %}
+{% endfor %}
+
+data-ciphers {% for algo in encryption.ncp_ciphers %}
+{{ encryption_map[algo] if algo in encryption_map.keys() else algo }}{% if not loop.last %}:{% endif %}
+{% endfor %}
{% endif %}
-{% endif %}
-{% if encryption.ncp_ciphers is defined and encryption.ncp_ciphers is not none %}
-data-ciphers {{ encryption.ncp_ciphers }}
-{% endif %}
{% endif %}
{% if hash is defined and hash is not none %}
auth {{ hash }}
{% endif %}
-keysize 256
-comp-lzo {{ '' if use_lzo_compression is defined else 'no' }}
+{{ 'comp-lzo' if use_lzo_compression is defined else '' }}
<ca>
-----BEGIN CERTIFICATE-----
@@ -79,7 +90,7 @@ comp-lzo {{ '' if use_lzo_compression is defined else 'no' }}
"""
-config = ConfigTreeQuery()
+config = Config()
base = ['interfaces', 'openvpn']
if not config.exists(base):
@@ -89,10 +100,22 @@ if not config.exists(base):
if __name__ == '__main__':
parser = argparse.ArgumentParser()
- parser.add_argument("-i", "--interface", type=str, help='OpenVPN interface the client is connecting to', required=True)
- parser.add_argument("-a", "--ca", type=str, help='OpenVPN CA cerificate', required=True)
- parser.add_argument("-c", "--cert", type=str, help='OpenVPN client cerificate', required=True)
- parser.add_argument("-k", "--key", type=str, help='OpenVPN client cerificate key', action="store")
+ parser.add_argument(
+ "-i",
+ "--interface",
+ type=str,
+ help='OpenVPN interface the client is connecting to',
+ required=True,
+ )
+ parser.add_argument(
+ "-a", "--ca", type=str, help='OpenVPN CA cerificate', required=True
+ )
+ parser.add_argument(
+ "-c", "--cert", type=str, help='OpenVPN client cerificate', required=True
+ )
+ parser.add_argument(
+ "-k", "--key", type=str, help='OpenVPN client cerificate key', action="store"
+ )
args = parser.parse_args()
interface = args.interface
@@ -114,33 +137,25 @@ if __name__ == '__main__':
if not config.exists(['pki', 'certificate', cert, 'private', 'key']):
exit(f'OpenVPN certificate key "{key}" does not exist!')
- ca = config.value(['pki', 'ca', ca, 'certificate'])
+ config = config.get_config_dict(
+ base + [interface],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True,
+ with_pki=True,
+ )
+
+ ca = config['pki']['ca'][ca]['certificate']
ca = fill(ca, width=64)
- cert = config.value(['pki', 'certificate', cert, 'certificate'])
+ cert = config['pki']['certificate'][cert]['certificate']
cert = fill(cert, width=64)
- key = config.value(['pki', 'certificate', key, 'private', 'key'])
+ key = config['pki']['certificate'][key]['private']['key']
key = fill(key, width=64)
- remote_host = config.value(base + [interface, 'local-host'])
-
- ovpn_conf = config.get_config_dict(base + [interface], key_mangling=('-', '_'), get_first_key=True)
-
- port = '1194' if 'local_port' not in ovpn_conf else ovpn_conf['local_port']
- proto = 'udp' if 'protocol' not in ovpn_conf else ovpn_conf['protocol']
- device = 'tun' if 'device_type' not in ovpn_conf else ovpn_conf['device_type']
-
- config = {
- 'interface' : interface,
- 'ca' : ca,
- 'cert' : cert,
- 'key' : key,
- 'device' : device,
- 'port' : port,
- 'proto' : proto,
- 'remote_host' : remote_host,
- 'address' : [],
- }
-
-# Clear out terminal first
-print('\x1b[2J\x1b[H')
-client = Template(client_config, trim_blocks=True).render(config)
-print(client)
+
+ config['ca'] = ca
+ config['cert'] = cert
+ config['key'] = key
+ config['port'] = '1194' if 'local_port' not in config else config['local_port']
+
+ client = Template(client_config, trim_blocks=True).render(config)
+ print(client)
diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py
index b193d8109..cf2bc6d5c 100755
--- a/src/op_mode/ikev2_profile_generator.py
+++ b/src/op_mode/ikev2_profile_generator.py
@@ -105,10 +105,39 @@ vyos2windows_integrity = {
}
# 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
-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']
+# be: 14, 15, 16, 17, 18, 19, 20, 21, 31, 32
+vyos2apple_dh_group = {
+ '14' : '14',
+ '15' : '15',
+ '16' : '16',
+ '17' : '17',
+ '18' : '18',
+ '19' : '19',
+ '20' : '20',
+ '21' : '21',
+ '31' : '31',
+ '32' : '32'
+}
+
+# Newer versions of Windows support groups 19 and 20, albeit under a different naming convention
+vyos2windows_dh_group = {
+ '1' : 'Group1',
+ '2' : 'Group2',
+ '14' : 'Group14',
+ '19' : 'ECP256',
+ '20' : 'ECP384',
+ '24' : 'Group24'
+}
+
+# For PFS, Windows also has its own inconsistent naming scheme for each group
+vyos2windows_pfs_group = {
+ '1' : 'PFS1',
+ '2' : 'PFS2',
+ '14' : 'PFS2048',
+ '19' : 'ECP256',
+ '20' : 'ECP384',
+ '24' : 'PFS24'
+}
parser = argparse.ArgumentParser()
parser.add_argument('--os', const='all', nargs='?', choices=['ios', 'windows'], help='Operating system used for config generation', required=True)
@@ -181,7 +210,7 @@ if args.os == 'ios':
# https://stackoverflow.com/a/9427216
data['ca_certificates'] = [dict(t) for t in {tuple(d.items()) for d in data['ca_certificates']}]
-esp_proposals = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group'], 'proposal'],
+esp_group = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group']],
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)
@@ -192,7 +221,29 @@ ike_proposal = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group']
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;
+vyos2client_dh_group = vyos2apple_dh_group if args.os == 'ios' else vyos2windows_dh_group
+
+def transform_pfs(pfs, ike_dh_group):
+ pfs_enabled = (pfs != 'disable')
+ if pfs == 'enable':
+ pfs_dh_group = ike_dh_group
+ elif pfs.startswith('dh-group'):
+ pfs_dh_group = pfs.removeprefix('dh-group')
+
+ if args.os == 'ios':
+ if pfs_enabled:
+ if pfs_dh_group not in set(vyos2apple_dh_group):
+ exit(f'The PFS group configured for "{args.connection}" is not supported by the client!')
+ return pfs_dh_group
+ else:
+ return None
+ else:
+ if pfs_enabled:
+ if pfs_dh_group not in set(vyos2windows_pfs_group):
+ exit(f'The PFS group configured for "{args.connection}" is not supported by the client!')
+ return vyos2windows_pfs_group[ pfs_dh_group ]
+ else:
+ return 'None'
# Create a dictionary containing client conform IKE settings
ike = {}
@@ -201,24 +252,28 @@ for _, proposal in ike_proposal.items():
if {'dh_group', 'encryption', 'hash'} <= set(proposal):
if (proposal['encryption'] in set(vyos2client_cipher) and
proposal['hash'] in set(vyos2client_integrity) and
- proposal['dh_group'] in set(supported_dh_groups)):
+ proposal['dh_group'] in set(vyos2client_dh_group)):
# We 're-code' from the VyOS IPsec proposals to the Apple naming scheme
proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
+ # DH group will need to be transformed later after we calculate PFS group
ike.update( { str(count) : proposal } )
count += 1
-# Create a dictionary containing Apple conform ESP settings
+# Create a dictionary containing client conform ESP settings
esp = {}
count = 1
-for _, proposal in esp_proposals.items():
+for _, proposal in esp_group['proposal'].items():
if {'encryption', 'hash'} <= set(proposal):
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'] = vyos2client_cipher[ proposal['encryption'] ]
proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
+ # Copy PFS setting from the group, if present (we will need to
+ # transform this later once the IKE group is selected)
+ proposal['pfs'] = esp_group.get('pfs', 'enable')
esp.update( { str(count) : proposal } )
count += 1
@@ -230,8 +285,10 @@ try:
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:
+ elif len(ike) == 1:
data['ike_encryption'] = ike['1']
+ else:
+ exit(f'None of the configured IKE proposals for "{args.connection}" are supported by the client!')
if len(esp) > 1:
tmp = '\n'
@@ -239,12 +296,18 @@ try:
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:
+ elif len(esp) == 1:
data['esp_encryption'] = esp['1']
+ else:
+ exit(f'None of the configured ESP proposals for "{args.connection}" are supported by the client!')
except KeyboardInterrupt:
exit("Interrupted")
+# Transform the DH and PFS groups now that all selections are known
+data['esp_encryption']['pfs'] = transform_pfs(data['esp_encryption']['pfs'], data['ike_encryption']['dh_group'])
+data['ike_encryption']['dh_group'] = vyos2client_dh_group[ data['ike_encryption']['dh_group'] ]
+
print('\n\n==== <snip> ====')
if args.os == 'ios':
print(render_to_string('ipsec/ios_profile.j2', data))