diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 2d280a5c6..e59f20a5d 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -1,411 +1,407 @@ #!/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 os from copy import deepcopy from subprocess import DEVNULL from sys import exit from time import sleep from vyos.config import Config from vyos.configdiff import ConfigDiff from vyos.template import render -from vyos.util import call, get_interface_address, process_named_running, run, cidr_fit +from vyos.util import call +from vyos.util import get_interface_address +from vyos.util import process_named_running +from vyos.util import run +from vyos.util import cidr_fit from vyos import ConfigError from vyos import airbag airbag.enable() authby_translate = { 'pre-shared-secret': 'secret', 'rsa': 'rsasig', 'x509': 'rsasig' } default_pfs = 'dh-group2' pfs_translate = { 'dh-group1': 'modp768', 'dh-group2': 'modp1024', 'dh-group5': 'modp1536', 'dh-group14': 'modp2048', 'dh-group15': 'modp3072', 'dh-group16': 'modp4096', 'dh-group17': 'modp6144', 'dh-group18': 'modp8192', 'dh-group19': 'ecp256', 'dh-group20': 'ecp384', 'dh-group21': 'ecp512', 'dh-group22': 'modp1024s160', 'dh-group23': 'modp2048s224', 'dh-group24': 'modp2048s256', 'dh-group25': 'ecp192', 'dh-group26': 'ecp224', 'dh-group27': 'ecp224bp', 'dh-group28': 'ecp256bp', 'dh-group29': 'ecp384bp', 'dh-group30': 'ecp512bp', 'dh-group31': 'curve25519', 'dh-group32': 'curve448' } any_log_modes = [ 'dmn', 'mgr', 'ike', 'chd','job', 'cfg', 'knl', 'net', 'asn', 'enc', 'lib', 'esp', 'tls', 'tnc', 'imc', 'imv', 'pts' ] ike_ciphers = {} esp_ciphers = {} mark_base = 0x900000 CA_PATH = "/etc/ipsec.d/cacerts/" CRL_PATH = "/etc/ipsec.d/crls/" DHCP_BASE = "/var/lib/dhcp/dhclient" LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/'] X509_PATH = '/config/auth/' conf = None def resync_l2tp(conf): if not conf.exists('vpn l2tp remote-access ipsec-settings '): return tmp = run('/usr/libexec/vyos/conf_mode/ipsec-settings.py') if tmp > 0: print('ERROR: failed to reapply L2TP IPSec settings!') def resync_nhrp(conf): if not conf.exists('protocols nhrp tunnel'): return run('/opt/vyatta/sbin/vyos-update-nhrp.pl --set_ipsec') def get_config(config=None): global conf if config: conf = config else: conf = Config() base = ['vpn', 'ipsec'] if not conf.exists(base): return None # retrieve common dictionary keys ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) default_ike_pfs = None if 'ike_group' in ipsec: for group, ike_conf in ipsec['ike_group'].items(): if 'proposal' in ike_conf: ciphers = [] for i in ike_conf['proposal']: proposal = ike_conf['proposal'][i] enc = proposal['encryption'] if 'encryption' in proposal else None hash = proposal['hash'] if 'hash' in proposal else None pfs = ('dh-group' + proposal['dh_group']) if 'dh_group' in proposal else default_pfs if not default_ike_pfs: default_ike_pfs = pfs if enc and hash: ciphers.append(f"{enc}-{hash}-{pfs_translate[pfs]}" if pfs else f"{enc}-{hash}") ike_ciphers[group] = ','.join(ciphers) + '!' if 'esp_group' in ipsec: for group, esp_conf in ipsec['esp_group'].items(): pfs = esp_conf['pfs'] if 'pfs' in esp_conf else 'enable' if pfs == 'disable': pfs = None if pfs == 'enable': pfs = default_ike_pfs if 'proposal' in esp_conf: ciphers = [] for i in esp_conf['proposal']: proposal = esp_conf['proposal'][i] enc = proposal['encryption'] if 'encryption' in proposal else None hash = proposal['hash'] if 'hash' in proposal else None if enc and hash: ciphers.append(f"{enc}-{hash}-{pfs_translate[pfs]}" if pfs else f"{enc}-{hash}") esp_ciphers[group] = ','.join(ciphers) + '!' return ipsec def verify(ipsec): if not ipsec: return None if 'profile' in ipsec: for profile, profile_conf in ipsec['profile'].items(): if 'esp_group' in profile_conf: if 'esp_group' not in ipsec or profile_conf['esp_group'] not in ipsec['esp_group']: raise ConfigError(f"Invalid esp-group on {profile} profile") else: raise ConfigError(f"Missing esp-group on {profile} profile") if 'ike_group' in profile_conf: if 'ike_group' not in ipsec or profile_conf['ike_group'] not in ipsec['ike_group']: raise ConfigError(f"Invalid ike-group on {profile} profile") else: raise ConfigError(f"Missing ike-group on {profile} profile") if 'authentication' not in profile_conf: raise ConfigError(f"Missing authentication on {profile} profile") if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']: for peer, peer_conf in ipsec['site_to_site']['peer'].items(): has_default_esp = False if 'default_esp_group' in peer_conf: has_default_esp = True if 'esp_group' not in ipsec or peer_conf['default_esp_group'] not in ipsec['esp_group']: raise ConfigError(f"Invalid esp-group on site-to-site peer {peer}") if 'ike_group' in peer_conf: if 'ike_group' not in ipsec or peer_conf['ike_group'] not in ipsec['ike_group']: raise ConfigError(f"Invalid ike-group on site-to-site peer {peer}") else: raise ConfigError(f"Missing ike-group on site-to-site peer {peer}") if 'authentication' not in peer_conf or 'mode' not in peer_conf['authentication']: raise ConfigError(f"Missing authentication on site-to-site peer {peer}") if peer_conf['authentication']['mode'] == 'x509': if 'x509' not in peer_conf['authentication']: raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}") if 'key' not in peer_conf['authentication']['x509'] or 'ca_cert_file' not in peer_conf['authentication']['x509'] or 'cert_file' not in peer_conf['authentication']['x509']: raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}") if 'file' not in peer_conf['authentication']['x509']['key']: raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}") for key in ['ca_cert_file', 'cert_file', 'crl_file']: if key in peer_conf['authentication']['x509']: path = peer_conf['authentication']['x509'][key] if not os.path.exists(path if path.startswith(X509_PATH) else (X509_PATH + path)): raise ConfigError(f"File not found for {key} on site-to-site peer {peer}") key_path = peer_conf['authentication']['x509']['key']['file'] if not os.path.exists(key_path if key_path.startswith(X509_PATH) else (X509_PATH + key_path)): raise ConfigError(f"Private key not found on site-to-site peer {peer}") if peer_conf['authentication']['mode'] == 'rsa': if not verify_rsa_local_key(): raise ConfigError(f"Invalid key on rsa-keys local-key") if 'rsa_key_name' not in peer_conf['authentication']: raise ConfigError(f"Missing rsa-key-name on site-to-site peer {peer}") if not verify_rsa_key(peer_conf['authentication']['rsa_key_name']): raise ConfigError(f"Invalid rsa-key-name on site-to-site peer {peer}") if 'local_address' not in peer_conf and 'dhcp_interface' not in peer_conf: raise ConfigError(f"Missing local-address or dhcp-interface on site-to-site peer {peer}") if 'dhcp_interface' in peer_conf: dhcp_interface = peer_conf['dhcp_interface'] if not os.path.exists(f'{DHCP_BASE}_{dhcp_interface}.conf'): raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}") if 'vti' in peer_conf: if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf: raise ConfigError(f"A single local-address or dhcp-interface is required when using VTI on site-to-site peer {peer}") if 'bind' in peer_conf['vti']: vti_interface = peer_conf['vti']['bind'] - if not get_vti_interface(vti_interface): - raise ConfigError(f'Invalid VTI interface on site-to-site peer {peer}') + if not os.path.exists(f'/sys/class/net/{vti_interface}'): + raise ConfigError(f'VTI interface {vti_interface} for site-to-site peer {peer} does not exist!') if 'vti' not in peer_conf and 'tunnel' not in peer_conf: raise ConfigError(f"No vti or tunnels specified on site-to-site peer {peer}") if 'tunnel' in peer_conf: for tunnel, tunnel_conf in peer_conf['tunnel'].items(): if 'esp_group' not in tunnel_conf and not has_default_esp: raise ConfigError(f"Missing esp-group on tunnel {tunnel} for site-to-site peer {peer}") esp_group_name = tunnel_conf['esp_group'] if 'esp_group' in tunnel_conf else peer_conf['default_esp_group'] if esp_group_name not in ipsec['esp_group']: raise ConfigError(f"Invalid esp-group on tunnel {tunnel} for site-to-site peer {peer}") esp_group = ipsec['esp_group'][esp_group_name] if 'mode' in esp_group and esp_group['mode'] == 'transport': if 'protocol' in tunnel_conf and ((peer in ['any', '0.0.0.0']) or ('local_address' not in peer_conf or peer_conf['local_address'] in ['any', '0.0.0.0'])): raise ConfigError(f"Fixed local-address or peer required when a protocol is defined with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}") if ('local' in tunnel_conf and 'prefix' in tunnel_conf['local']) or ('remote' in tunnel_conf and 'prefix' in tunnel_conf['remote']): raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}") def get_rsa_local_key(): global conf base = ['vpn', 'rsa-keys'] if not conf.exists(base + ['local-key', 'file']): return False return conf.return_value(base + ['local-key', 'file']) def verify_rsa_local_key(): file = get_rsa_local_key() if not file: return False for path in LOCAL_KEY_PATHS: if os.path.exists(path + file): return path + file return False def verify_rsa_key(key_name): global conf base = ['vpn', 'rsa-keys'] if not conf.exists(base): return False return conf.exists(base + ['rsa-key-name', key_name, 'rsa-key']) def generate(ipsec): data = {} if ipsec: data = deepcopy(ipsec) data['authby'] = authby_translate data['ciphers'] = {'ike': ike_ciphers, 'esp': esp_ciphers} data['marks'] = {} data['rsa_local_key'] = verify_rsa_local_key() data['x509_path'] = X509_PATH if 'site_to_site' in data and 'peer' in data['site_to_site']: for peer, peer_conf in ipsec['site_to_site']['peer'].items(): if peer_conf['authentication']['mode'] == 'x509': ca_cert_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['ca_cert_file']) call(f'cp -f {ca_cert_file} {CA_PATH}') if 'crl_file' in peer_conf['authentication']['x509']: crl_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['crl_file']) call(f'cp -f {crl_file} {CRL_PATH}') local_ip = '' if 'local_address' in peer_conf: local_ip = peer_conf['local_address'] elif 'dhcp_interface' in peer_conf: local_ip = get_dhcp_address(peer_conf['dhcp_interface']) data['site_to_site']['peer'][peer]['local_address'] = local_ip if 'vti' in peer_conf and 'bind' in peer_conf['vti']: vti_interface = peer_conf['vti']['bind'] data['marks'][vti_interface] = get_mark(vti_interface) else: for tunnel, tunnel_conf in peer_conf['tunnel'].items(): if ('local' not in tunnel_conf or 'prefix' not in tunnel_conf['local']) or ('remote' not in tunnel_conf or 'prefix' not in tunnel_conf['remote']): continue local_prefix = tunnel_conf['local']['prefix'] remote_prefix = tunnel_conf['remote']['prefix'] passthrough = cidr_fit(local_prefix, remote_prefix) data['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough if 'logging' in ipsec and 'log_modes' in ipsec['logging']: modes = ipsec['logging']['log_modes'] level = ipsec['logging']['log_level'] if 'log_level' in ipsec['logging'] else '1' if isinstance(modes, str): modes = [modes] if 'any' in modes: modes = any_log_modes data['charondebug'] = f' {level}, '.join(modes) + ' ' + level render("/etc/ipsec.conf", "ipsec/ipsec.conf.tmpl", data) render("/etc/ipsec.secrets", "ipsec/ipsec.secrets.tmpl", data) render("/etc/swanctl/swanctl.conf", "ipsec/swanctl.conf.tmpl", data) def apply(ipsec): if not ipsec: if conf.exists('vpn l2tp '): call('sudo /usr/sbin/ipsec rereadall') call('sudo /usr/sbin/ipsec reload') call('sudo /usr/sbin/swanctl -q') else: call('sudo /usr/sbin/ipsec stop') resync_l2tp(conf) resync_nhrp(conf) return diff = ConfigDiff(conf, key_mangling=('-', '_')) diff.set_level(['vpn', 'ipsec']) old_if, new_if = diff.get_value_diff(['ipsec-interfaces', 'interface']) interface_change = (old_if != new_if) should_start = ('profile' in ipsec or ('site_to_site' in ipsec and 'peer' in ipsec['site_to_site'])) if not process_named_running('charon'): args = '' if 'auto_update' in ipsec: args = f'--auto-update {ipsec["auto_update"]}' if should_start: call(f'sudo /usr/sbin/ipsec start {args}') else: if not should_start: call('sudo /usr/sbin/ipsec stop') elif interface_change: call('sudo /usr/sbin/ipsec restart') else: call('sudo /usr/sbin/ipsec rereadall') call('sudo /usr/sbin/ipsec reload') if should_start: sleep(2) # Give charon enough time to start call('sudo /usr/sbin/swanctl -q') resync_l2tp(conf) resync_nhrp(conf) -def get_vti_interface(vti_interface): - global conf - section = conf.get_config_dict(['interfaces', 'vti'], get_first_key=True) - for interface, interface_conf in section.items(): - if interface == vti_interface: - return interface_conf - return None - def get_mark(vti_interface): vti_num = int(vti_interface.lstrip('vti')) return mark_base + vti_num def get_dhcp_address(interface): addr = get_interface_address(interface) if not addr: return None if len(addr['addr_info']) == 0: return None return addr['addr_info'][0]['local'] if __name__ == '__main__': try: ipsec = get_config() verify(ipsec) generate(ipsec) apply(ipsec) except ConfigError as e: print(e) exit(1)