diff --git a/data/templates/ipsec/charon/dhcp.conf.tmpl b/data/templates/ipsec/charon/dhcp.conf.tmpl new file mode 100644 index 000000000..2879550a8 --- /dev/null +++ b/data/templates/ipsec/charon/dhcp.conf.tmpl @@ -0,0 +1,23 @@ +dhcp { + load = yes + +{% if options is defined and options.remote_access is defined and options.remote_access.dhcp_pool is defined %} +{% if options.remote_access.dhcp_pool.interface is defined %} + interface = {{ options.remote_access.dhcp_pool.interface }} +{% endif %} +{% if options.remote_access.dhcp_pool.server is defined %} + server = {{ options.remote_access.dhcp_pool.server }} +{% endif %} +{% endif %} + + # Always use the configured server address. + # force_server_address = no + + # Derive user-defined MAC address from hash of IKE identity and send client + # identity DHCP option. + # identity_lease = no + + # Use the DHCP server port (67) as source port when a unicast server address + # is configured. + # use_server_port = no +} diff --git a/data/templates/ipsec/swanctl.conf.tmpl b/data/templates/ipsec/swanctl.conf.tmpl index 0eda8479a..00251d44d 100644 --- a/data/templates/ipsec/swanctl.conf.tmpl +++ b/data/templates/ipsec/swanctl.conf.tmpl @@ -1,96 +1,106 @@ ### Autogenerated by vpn_ipsec.py ### {% import 'ipsec/swanctl/profile.tmpl' as profile_tmpl %} {% import 'ipsec/swanctl/peer.tmpl' as peer_tmpl %} {% import 'ipsec/swanctl/remote_access.tmpl' as remote_access_tmpl %} connections { {% if profile is defined %} {% for name, profile_conf in profile.items() if profile_conf.disable is not defined and profile_conf.bind is defined and profile_conf.bind.tunnel is defined %} {{ profile_tmpl.conn(name, profile_conf, ike_group, esp_group) }} {% endfor %} {% endif %} {% if site_to_site is defined and site_to_site.peer is defined %} {% for peer, peer_conf in site_to_site.peer.items() if peer not in dhcp_no_address and peer_conf.disable is not defined %} {{ peer_tmpl.conn(peer, peer_conf, ike_group, esp_group) }} {% endfor %} {% endif %} {% if remote_access is defined and remote_access is not none %} {% for rw, rw_conf in remote_access.items() if rw_conf.disable is not defined %} {{ remote_access_tmpl.conn(rw, rw_conf, ike_group, esp_group) }} {% endfor %} {% endif %} } pools { {% if remote_access is defined %} -{% for ra, ra_conf in remote_access.items() if remote_access is defined %} +{% for ra, ra_conf in remote_access.items() if ra_conf.pool.dhcp_enable is not defined %} ra-{{ ra }} { addrs = {{ ra_conf.pool.prefix }} dns = {{ ra_conf.pool.name_server | join(",") }} {% if ra_conf.pool.exclude is defined %} split_exclude = {{ ra_conf.pool.exclude | join(",") }} {% endif %} } {% endfor %} {% endif %} } secrets { {% if profile is defined %} {% for name, profile_conf in profile.items() if profile_conf.disable is not defined and profile_conf.bind is defined and profile_conf.bind.tunnel is defined %} {% if profile_conf.authentication.mode == 'pre-shared-secret' %} {% for interface in profile_conf.bind.tunnel %} ike-dmvpn-{{ interface }} { secret = {{ profile_conf.authentication.pre_shared_secret }} } {% endfor %} {% endif %} {% endfor %} {% endif %} {% if site_to_site is defined and site_to_site.peer is defined %} {% set ns = namespace(local_key_set=False) %} {% for peer, peer_conf in site_to_site.peer.items() if peer not in dhcp_no_address and peer_conf.disable is not defined %} {% set peer_name = peer.replace(".", "-").replace("@", "") %} {% if peer_conf.authentication.mode == 'pre-shared-secret' %} ike_{{ peer_name }} { {% if peer_conf.local_address is defined %} id-local = {{ peer_conf.local_address }} # dhcp:{{ peer_conf.dhcp_interface if 'dhcp_interface' in peer_conf else 'no' }} {% endif %} id-remote = {{ peer }} {% if peer_conf.authentication.id is defined %} id-localid = {{ peer_conf.authentication.id }} {% endif %} {% if peer_conf.authentication.remote_id is defined %} id-remoteid = {{ peer_conf.authentication.remote_id }} {% endif %} secret = "{{ peer_conf.authentication.pre_shared_secret }}" } {% elif peer_conf.authentication.mode == 'x509' %} private_{{ peer_name }} { file = {{ peer_conf.authentication.x509.certificate }}.pem {% if peer_conf.authentication.x509.passphrase is defined %} secret = "{{ peer_conf.authentication.x509.passphrase }}" {% endif %} } {% elif peer_conf.authentication.mode == 'rsa' and not ns.local_key_set %} {% set ns.local_key_set = True %} rsa_local { file = {{ rsa_local_key }} } {% endif %} {% endfor %} {% endif %} {% if remote_access is defined %} {% for ra, ra_conf in remote_access.items() if remote_access is defined %} -{% if ra_conf.authentication is defined and ra_conf.authentication.local_users is defined and ra_conf.authentication.local_users.username is defined %} +{% if ra_conf.authentication.server_mode == 'pre-shared-secret' %} + ike_{{ ra }} { +{% if ra_conf.authentication.id is defined %} + id = "{{ ra_conf.authentication.id }}" +{% elif ra_conf.local_address is defined %} + id = "{{ ra_conf.local_address }}" +{% endif %} + secret = "{{ ra_conf.authentication.pre_shared_secret }}" + } +{% endif %} +{% if ra_conf.authentication.client_mode == 'eap-mschapv2' and ra_conf.authentication.local_users is defined and ra_conf.authentication.local_users.username is defined %} {% for user, user_conf in ra_conf.authentication.local_users.username.items() if user_conf.disable is not defined %} eap-{{ ra }}-{{ user }} { secret = "{{ user_conf.password }}" id-{{ ra }}-{{ user }} = "{{ user }}" } {% endfor %} {% endif %} {% endfor %} {% endif %} } diff --git a/data/templates/ipsec/swanctl/remote_access.tmpl b/data/templates/ipsec/swanctl/remote_access.tmpl index a3a1cf0b2..95f2108fb 100644 --- a/data/templates/ipsec/swanctl/remote_access.tmpl +++ b/data/templates/ipsec/swanctl/remote_access.tmpl @@ -1,40 +1,48 @@ {% macro conn(name, rw_conf, ike_group, esp_group) %} {# peer needs to reference the global IKE configuration for certain values #} {% set ike = ike_group[rw_conf.ike_group] %} {% set esp = esp_group[rw_conf.esp_group] %} ra-{{ name }} { remote_addrs = %any local_addrs = {{ rw_conf.local_address if rw_conf.local_address is defined else '%any' }} proposals = {{ ike_group[rw_conf.ike_group] | get_esp_ike_cipher | join(',') }} version = {{ ike.key_exchange[4:] if ike is defined and ike.key_exchange is defined else "0" }} send_certreq = no rekey_time = {{ ike.lifetime }}s keyingtries = 0 +{% if rw_conf.pool.dhcp_enable is defined %} + pools = dhcp +{% else %} pools = ra-{{ name }} +{% endif %} local { - auth = pubkey -{% if rw_conf.authentication is defined and rw_conf.authentication.id is defined and rw_conf.authentication.use_x509_id is not defined %} +{% if rw_conf.authentication.id is defined and rw_conf.authentication.use_x509_id is not defined %} id = "{{ rw_conf.authentication.id }}" {% endif %} -{% if rw_conf.authentication is defined and rw_conf.authentication.x509 is defined and rw_conf.authentication.x509.certificate is defined %} +{% if rw_conf.authentication.server_mode == 'x509' %} + auth = pubkey certs = {{ rw_conf.authentication.x509.certificate }}.pem +{% elif rw_conf.authentication.server_mode == 'pre-shared-secret' %} + auth = psk {% endif %} } remote { - auth = eap-mschapv2 + auth = {{ rw_conf.authentication.client_mode }} +{% if rw_conf.authentication.client_mode.startswith("eap") %} eap_id = %any +{% endif %} } children { ikev2-vpn { esp_proposals = {{ esp | get_esp_ike_cipher | join(',') }} rekey_time = {{ esp.lifetime }}s rand_time = 540s dpd_action = clear {% set local_prefix = rw_conf.local.prefix if rw_conf.local is defined and rw_conf.local.prefix is defined else ['0.0.0.0/0', '::/0'] %} {% set local_port = rw_conf.local.port if rw_conf.local is defined and rw_conf.local.port is defined else '' %} {% set local_suffix = '[%any/{1}]'.format(local_port) if local_port else '' %} local_ts = {{ local_prefix | join(local_suffix + ",") }}{{ local_suffix }} } } } {% endmacro %} diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index f6b18d1d5..4425ab02a 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -1,1043 +1,1125 @@ Virtual Private Network (VPN) VPN IP security (IPsec) parameters 901 Set auto-update interval for IPsec daemon u32:30-65535 Auto-update interval (s) Option to disable requirement for unique IDs in the Security Database Name of Encapsulating Security Payload (ESP) group ESP compression disable enable disable Disable ESP compression (default) enable Enable ESP compression ^(disable|enable)$ ESP lifetime u32:30-86400 ESP lifetime in seconds (default 3600) 3600 ESP mode tunnel transport tunnel Tunnel mode (default) transport Transport mode ^(tunnel|transport)$ tunnel ESP Perfect Forward Secrecy enable dh-group1 dh-group2 dh-group5 dh-group14 dh-group15 dh-group16 dh-group17 dh-group18 dh-group19 dh-group20 dh-group21 dh-group22 dh-group23 dh-group24 dh-group25 dh-group26 dh-group27 dh-group28 dh-group29 dh-group30 dh-group31 dh-group32 disable enable Use Diffie-Hellman group 2 (modp1024) - default dh-group1 Use Diffie-Hellman group 1 (modp768) dh-group2 Use Diffie-Hellman group 2 (modp1024) dh-group5 Use Diffie-Hellman group 5 (modp1536) dh-group14 Use Diffie-Hellman group 14 (modp2048) dh-group15 Use Diffie-Hellman group 15 (modp3072) dh-group16 Use Diffie-Hellman group 16 (modp4096) dh-group17 Use Diffie-Hellman group 17 (modp6144) dh-group18 Use Diffie-Hellman group 18 (modp8192) dh-group19 Use Diffie-Hellman group 19 (ecp256) dh-group20 Use Diffie-Hellman group 20 (ecp384) dh-group21 Use Diffie-Hellman group 21 (ecp521) dh-group22 Use Diffie-Hellman group 22 (modp1024s160) dh-group23 Use Diffie-Hellman group 23 (modp2048s224) dh-group24 Use Diffie-Hellman group 24 (modp2048s256) dh-group25 Use Diffie-Hellman group 25 (ecp192) dh-group26 Use Diffie-Hellman group 26 (ecp224) dh-group27 Use Diffie-Hellman group 27 (ecp224bp) dh-group28 Use Diffie-Hellman group 28 (ecp256bp) dh-group29 Use Diffie-Hellman group 29 (ecp384bp) dh-group30 Use Diffie-Hellman group 30 (ecp512bp) dh-group31 Use Diffie-Hellman group 31 (curve25519) dh-group32 Use Diffie-Hellman group 32 (curve448) disable Disable PFS ^(enable|dh-group1|dh-group2|dh-group5|dh-group14|dh-group15|dh-group16|dh-group17|dh-group18|dh-group19|dh-group20|dh-group21|dh-group22|dh-group23|dh-group24|dh-group25|dh-group26|dh-group27|dh-group28|dh-group29|dh-group30|dh-group31|dh-group32|disable)$ enable ESP-group proposal [REQUIRED] u32:1-65535 ESP-group proposal number #include #include Name of Internet Key Exchange (IKE) group close-action_help none hold clear restart none Set action to none (default) hold Set action to hold clear Set action to clear restart Set action to restart ^(none|hold|clear|restart)$ Dead Peer Detection (DPD) Keep-alive failure action hold clear restart hold Set action to hold (default) clear Set action to clear restart Set action to restart ^(hold|clear|restart)$ Keep-alive interval u32:2-86400 Keep-alive interval in seconds (default 30) Dead-Peer-Detection keep-alive timeout (IKEv1 only) u32:2-86400 Keep-alive timeout in seconds (default 120) ikev2-reauth_help yes no yes Enable remote host re-authentication during an IKE rekey. Currently broken due to a strongswan bug no Disable remote host re-authenticaton during an IKE rekey. (Default) ^(yes|no)$ Key Exchange Version ikev1 ikev2 ikev1 Use IKEv1 for Key Exchange [DEFAULT] ikev2 Use IKEv2 for Key Exchange ^(ikev1|ikev2)$ IKE lifetime u32:30-86400 IKE lifetime in seconds (default 28800) 28800 Enable MOBIKE Support. MOBIKE is only available for IKEv2. enable disable enable Enable MOBIKE (default for IKEv2) disable Disable MOBIKE ^(enable|disable)$ IKEv1 Phase 1 Mode Selection main aggressive main Use Main mode for Key Exchanges in the IKEv1 Protocol (Recommended Default) aggressive Use Aggressive mode for Key Exchanges in the IKEv1 protocol - We do not recommend users to use aggressive mode as it is much more insecure compared to Main mode. ^(main|aggressive)$ proposal_help u32:1-65535 IKE-group proposal 2 dh-grouphelp 1 2 5 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 1 Diffie-Hellman group 1 (modp768) 2 Diffie-Hellman group 2 (modp1024) 5 Diffie-Hellman group 5 (modp1536) 14 Diffie-Hellman group 14 (modp2048) 15 Diffie-Hellman group 15 (modp3072) 16 Diffie-Hellman group 16 (modp4096) 17 Diffie-Hellman group 17 (modp6144) 18 Diffie-Hellman group 18 (modp8192) 19 Diffie-Hellman group 19 (ecp256) 20 Diffie-Hellman group 20 (ecp384) 21 Diffie-Hellman group 21 (ecp521) 22 Diffie-Hellman group 22 (modp1024s160) 23 Diffie-Hellman group 23 (modp2048s224) 24 Diffie-Hellman group 24 (modp2048s256) 25 Diffie-Hellman group 25 (ecp192) 26 Diffie-Hellman group 26 (ecp224) 27 Diffie-Hellman group 27 (ecp224bp) 28 Diffie-Hellman group 28 (ecp256bp) 29 Diffie-Hellman group 29 (ecp384bp) 30 Diffie-Hellman group 30 (ecp512bp) 31 Diffie-Hellman group 31 (curve25519) 32 Diffie-Hellman group 32 (curve448) ^(1|2|5|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32)$ #include #include Sets to include an additional configuration directive file for strongSwan. Use an absolute path to specify the included file Sets to include an additional secrets file for strongSwan. Use an absolute path to specify the included file. Interface to use for VPN [REQUIRED] IPsec interface [REQUIRED] IPsec logging strongSwan Logger Level u32:0 Very basic auditing logs e.g. SA up/SA down (default) u32:1 Generic control flow with errors, a good default to see whats going on u32:2 More detailed debugging control flow 0 Subsystem in the daemon the log comes from dmn mgr ike chd job cfg knl net asn enc lib esp tls tnc imc imv pts any dmn Main daemon setup/cleanup/signal handling mgr IKE_SA manager, handling synchronization for IKE_SA access ike IKE_SA/ISAKMP SA chd CHILD_SA/IPsec SA job Jobs queuing/processing and thread pool management cfg Configuration management and plugins knl IPsec/Networking kernel interface net IKE network communication asn Low-level encoding/decoding (ASN.1, X.509 etc.) enc Packet encoding/decoding encryption/decryption operations lib libstrongswan library messages esp libipsec library messages tls libtls library messages tnc Trusted Network Connect imc Integrity Measurement Collector imv Integrity Measurement Verifier pts Platform Trust Service any Any subsystem ^(dmn|mgr|ike|chd|job|cfg|knl|net|asn|enc|lib|esp|tls|tnc|imc|imv|pts|any)$ Global IPsec settings Do not automatically install routes to remote networks + + + remote-access global options + + + + + DHCP pool options for remote-access + + + + + Interface with DHCP server to use + + + + + + + + DHCP server address + + ipv4 + IPv4 address of the DHCP server + + + + + + + VPN IPSec Profile #include Authentication [REQUIRED] Authentication mode pre-shared-secret pre-shared-secret Use pre shared secret key Pre-shared secret key txt Pre-shared secret key DMVPN crypto configuration Tunnel interface associated with this configuration profile interfaces tunnel txt Associated interface to this configuration profile #include #include Remote access IKEv2 VPN Authentication for remote access #include #include + + + Client authentication mode + + eap-tls eap-mschapv2 + + + eap-tls + EAP-TLS + + + eap-mschapv2 + EAP-MSCHAPv2 + + + ^(eap-tls|eap-mschapv2)$ + + + eap-mschapv2 + Local user authentication for PPPoE server User name for authentication #include Password for authentication + + + Server authentication mode + + pre-shared-secret x509 + + + pre-shared-secret + pre-shared-secret_description + + + x509 + x509_description + + + ^(pre-shared-secret|x509)$ + + + x509 + + + + Pre-shared-secret used for server authentication + + #include #include #include #include #include #include IP address pool for remote-access users + + + Enable DHCP pool for clients on this connection + + + Local IPv4 or IPv6 pool prefix exclusions ipv4 Local IPv4 pool prefix exclusion ipv6 Local IPv6 pool prefix exclusion Local IPv4 or IPv6 pool prefix ipv4 Local IPv4 pool prefix ipv6 Local IPv6 pool prefix #include Timeout to close connection if no data is transmitted u32:10-86400 Timeout in seconds (default 28800) 28800 Site-to-site VPN VPN peer ipv4 IPv4 address of the peer ipv6 IPv6 address of the peer txt Hostname of the peer <@text> ID of the peer #include Peer authentication [REQUIRED] #include #include Authentication mode pre-shared-secret rsa x509 pre-shared-secret pre-shared-secret_description rsa rsa_description x509 x509_description ^(pre-shared-secret|rsa|x509)$ Pre-shared secret key txt Pre-shared secret key ID for remote authentication txt ID used for peer authentication RSA key name Use certificate common name as ID Connection type initiate respond initiate initiate_description respond respond_description ^(initiate|respond)$ Defult ESP group name vpn ipsec esp-group #include DHCP interface to listen on Force UDP Encapsulation for ESP Payloads enable disable enable This endpoint will force UDP encapsulation for this peer disable This endpoint will not force UDP encapsulation for this peer ^(enable|disable)$ #include Re-authentication of the remote peer during an IKE re-key. IKEv2 option only yes no inherit yes Enable remote host re-autentication during an IKE re-key. Currently broken due to a strong swan bug no Disable remote host re-authenticaton during an IKE re-key. inherit Inherit the reauth configuration form your IKE-group (Default) ^(yes|no|inherit)$ #include Peer tunnel [REQUIRED] u32 Peer tunnel [REQUIRED] #include #include #include #include Remote parameters for interesting traffic #include Remote IPv4 or IPv6 prefix ipv4 Remote IPv4 prefix ipv6 Remote IPv6 prefix Virtual tunnel interface [REQUIRED] VTI tunnel interface associated with this configuration interfaces vti #include diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 76ee64a20..cf23a89c6 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -1,418 +1,462 @@ #!/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 . import ipaddress import os from sys import exit from time import sleep from vyos.config import Config from vyos.configdict import leaf_node_changed from vyos.configverify import verify_interface_exists from vyos.configdict import dict_merge from vyos.ifconfig import Interface from vyos.pki import wrap_certificate from vyos.pki import wrap_crl from vyos.pki import wrap_public_key from vyos.pki import wrap_private_key from vyos.template import ip_from_cidr from vyos.template import render from vyos.validate import is_ipv6_link_local from vyos.util import call from vyos.util import dict_search_args from vyos.util import run from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() dhcp_wait_attempts = 2 dhcp_wait_sleep = 1 swanctl_dir = '/etc/swanctl' ipsec_conf = '/etc/ipsec.conf' ipsec_secrets = '/etc/ipsec.secrets' +charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf' interface_conf = '/etc/strongswan.d/interfaces_use.conf' swanctl_conf = f'{swanctl_dir}/swanctl.conf' CERT_PATH = f'{swanctl_dir}/x509/' KEY_PATH = f'{swanctl_dir}/private/' CA_PATH = f'{swanctl_dir}/x509ca/' CRL_PATH = f'{swanctl_dir}/x509crl/' DHCP_BASE = '/var/lib/dhcp/dhclient' DHCP_HOOK_IFLIST = '/tmp/ipsec_dhcp_waiting' LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/'] X509_PATH = '/config/auth/' def get_config(config=None): 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) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = defaults(base) # XXX: T2665: we must safely remove default values for tag nodes, those are # added in a more fine grained way later on del default_values['esp_group'] del default_values['ike_group'] del default_values['remote_access'] ipsec = dict_merge(default_values, ipsec) if 'esp_group' in ipsec: default_values = defaults(base + ['esp-group']) for group in ipsec['esp_group']: ipsec['esp_group'][group] = dict_merge(default_values, ipsec['esp_group'][group]) if 'ike_group' in ipsec: default_values = defaults(base + ['ike-group']) for group in ipsec['ike_group']: ipsec['ike_group'][group] = dict_merge(default_values, ipsec['ike_group'][group]) if 'remote_access' in ipsec: default_values = defaults(base + ['remote-access']) for rw in ipsec['remote_access']: ipsec['remote_access'][rw] = dict_merge(default_values, ipsec['remote_access'][rw]) ipsec['dhcp_no_address'] = {} ipsec['interface_change'] = leaf_node_changed(conf, base + ['ipsec-interfaces', 'interface']) ipsec['l2tp_exists'] = conf.exists(['vpn', 'l2tp', 'remote-access', 'ipsec-settings']) ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel']) ipsec['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) ipsec['rsa_keys'] = conf.get_config_dict(['vpn', 'rsa-keys'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) return ipsec def get_rsa_local_key(ipsec): return dict_search_args(ipsec['rsa_keys'], 'local_key', 'file') def verify_rsa_local_key(ipsec): file = get_rsa_local_key(ipsec) if not file: return False for path in LOCAL_KEY_PATHS: full_path = os.path.join(path, file) if os.path.exists(full_path): return full_path return False def verify_rsa_key(ipsec, key_name): return dict_search_args(ipsec['rsa_keys'], 'rsa_key_name', key_name, 'rsa_key') def get_dhcp_address(iface): addresses = Interface(iface).get_addr() if not addresses: return None for address in addresses: if not is_ipv6_link_local(address): return ip_from_cidr(address) return None def verify_pki(pki, x509_conf): if not pki or 'ca' not in pki or 'certificate' not in pki: raise ConfigError(f'PKI is not configured') ca_cert_name = x509_conf['ca_certificate'] cert_name = x509_conf['certificate'] - if not dict_search_args(ipsec['pki'], 'ca', ca_cert_name, 'certificate'): + if not dict_search_args(pki, 'ca', ca_cert_name, 'certificate'): raise ConfigError(f'Missing CA certificate on specified PKI CA certificate "{ca_cert_name}"') - if not dict_search_args(ipsec['pki'], 'certificate', cert_name, 'certificate'): + if not dict_search_args(pki, 'certificate', cert_name, 'certificate'): raise ConfigError(f'Missing certificate on specified PKI certificate "{cert_name}"') - if not dict_search_args(ipsec['pki'], 'certificate', cert_name, 'private', 'key'): + if not dict_search_args(pki, 'certificate', cert_name, 'private', 'key'): raise ConfigError(f'Missing private key on specified PKI certificate "{cert_name}"') return True def verify(ipsec): if not ipsec: return None if 'ipsec_interfaces' in ipsec and 'interface' in ipsec['ipsec_interfaces']: interfaces = ipsec['ipsec_interfaces']['interface'] if isinstance(interfaces, str): interfaces = [interfaces] for ifname in interfaces: verify_interface_exists(ifname) 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 'remote_access' in ipsec: + for name, ra_conf in ipsec['remote_access'].items(): + if 'esp_group' in ra_conf: + if 'esp_group' not in ipsec or ra_conf['esp_group'] not in ipsec['esp_group']: + raise ConfigError(f"Invalid esp-group on {name} remote-access config") + else: + raise ConfigError(f"Missing esp-group on {name} remote-access config") + + if 'ike_group' in ra_conf: + if 'ike_group' not in ipsec or ra_conf['ike_group'] not in ipsec['ike_group']: + raise ConfigError(f"Invalid ike-group on {name} remote-access config") + else: + raise ConfigError(f"Missing ike-group on {name} remote-access config") + + if 'authentication' not in ra_conf: + raise ConfigError(f"Missing authentication on {name} remote-access config") + + if ra_conf['authentication']['server_mode'] == 'x509': + if 'x509' not in ra_conf['authentication']: + raise ConfigError(f"Missing x509 settings on {name} remote-access config") + + x509 = ra_conf['authentication']['x509'] + + if 'ca_certificate' not in x509 or 'certificate' not in x509: + raise ConfigError(f"Missing x509 certificates on {name} remote-access config") + + verify_pki(ipsec['pki'], x509) + elif ra_conf['authentication']['server_mode'] == 'pre-shared-secret': + if 'pre_shared_secret' not in ra_conf['authentication']: + raise ConfigError(f"Missing pre-shared-key on {name} remote-access config") + 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}") x509 = peer_conf['authentication']['x509'] if 'ca_certificate' not in x509 or 'certificate' not in x509: raise ConfigError(f"Missing x509 certificates on site-to-site peer {peer}") verify_pki(ipsec['pki'], x509) if peer_conf['authentication']['mode'] == 'rsa': if not verify_rsa_local_key(ipsec): 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(ipsec, 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'] verify_interface_exists(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}") address = get_dhcp_address(dhcp_interface) count = 0 while not address and count < dhcp_wait_attempts: address = get_dhcp_address(dhcp_interface) count += 1 sleep(dhcp_wait_sleep) if not address: ipsec['dhcp_no_address'][peer] = dhcp_interface print(f"Failed to get address from dhcp-interface on site-to-site peer {peer} -- skipped") continue 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 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 tunnel 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 cleanup_pki_files(): + for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH]: + if not os.path.exists(path): + continue + for file in os.listdir(path): + file_path = os.path.join(path, file) + if os.path.isfile(file_path): + os.unlink(file_path) + def generate_pki_files(pki, x509_conf): ca_cert_name = x509_conf['ca_certificate'] ca_cert_data = dict_search_args(pki, 'ca', ca_cert_name, 'certificate') ca_cert_crls = dict_search_args(pki, 'ca', ca_cert_name, 'crl') or [] crl_index = 1 cert_name = x509_conf['certificate'] cert_data = dict_search_args(pki, 'certificate', cert_name, 'certificate') key_data = dict_search_args(pki, 'certificate', cert_name, 'private', 'key') protected = 'passphrase' in x509_conf with open(os.path.join(CA_PATH, f'{ca_cert_name}.pem'), 'w') as f: f.write(wrap_certificate(ca_cert_data)) for crl in ca_cert_crls: with open(os.path.join(CRL_PATH, f'{ca_cert_name}_{crl_index}.pem'), 'w') as f: f.write(wrap_crl(crl)) crl_index += 1 with open(os.path.join(CERT_PATH, f'{cert_name}.pem'), 'w') as f: f.write(wrap_certificate(cert_data)) with open(os.path.join(KEY_PATH, f'{cert_name}.pem'), 'w') as f: f.write(wrap_private_key(key_data, protected)) def generate(ipsec): + cleanup_pki_files() + if not ipsec: for config_file in [ipsec_conf, ipsec_secrets, interface_conf, swanctl_conf]: if os.path.isfile(config_file): os.unlink(config_file) return if ipsec['dhcp_no_address']: with open(DHCP_HOOK_IFLIST, 'w') as f: f.write(" ".join(ipsec['dhcp_no_address'].values())) data = ipsec data['rsa_local_key'] = verify_rsa_local_key(ipsec) for path in [swanctl_dir, CERT_PATH, CA_PATH, CRL_PATH]: if not os.path.exists(path): os.mkdir(path, mode=0o755) if not os.path.exists(KEY_PATH): os.mkdir(KEY_PATH, mode=0o700) if 'remote_access' in ipsec: for rw, rw_conf in ipsec['remote_access'].items(): if 'authentication' in rw_conf and 'x509' in rw_conf['authentication']: generate_pki_files(ipsec['pki'], rw_conf['authentication']['x509']) 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 in ipsec['dhcp_no_address']: continue if peer_conf['authentication']['mode'] == 'x509': generate_pki_files(ipsec['pki'], peer_conf['authentication']['x509']) 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 'tunnel' in peer_conf: for tunnel, tunnel_conf in peer_conf['tunnel'].items(): local_prefixes = dict_search_args(tunnel_conf, 'local', 'prefix') remote_prefixes = dict_search_args(tunnel_conf, 'remote', 'prefix') if not local_prefixes or not remote_prefixes: continue passthrough = [] for local_prefix in local_prefixes: for remote_prefix in remote_prefixes: local_net = ipaddress.ip_network(local_prefix) remote_net = ipaddress.ip_network(remote_prefix) if local_net.overlaps(remote_net): passthrough.append(local_prefix) data['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough render(ipsec_conf, 'ipsec/ipsec.conf.tmpl', data) render(ipsec_secrets, 'ipsec/ipsec.secrets.tmpl', data) + render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.tmpl', data) render(interface_conf, 'ipsec/interfaces_use.conf.tmpl', data) render(swanctl_conf, 'ipsec/swanctl.conf.tmpl', data) def resync_l2tp(ipsec): if ipsec and not ipsec['l2tp_exists']: 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(ipsec): if ipsec and not ipsec['nhrp_exists']: return tmp = run('/usr/libexec/vyos/conf_mode/protocols_nhrp.py') if tmp > 0: print('ERROR: failed to reapply NHRP settings!') def apply(ipsec): if not ipsec: call('sudo ipsec stop') else: args = '' if 'auto_update' in ipsec: args = '--auto-update ' + ipsec['auto_update'] call(f'sudo ipsec restart {args}') call('sudo ipsec rereadall') call('sudo ipsec reload') sleep(5) # Give charon enough time to start call('sudo swanctl -q') resync_l2tp(ipsec) resync_nhrp(ipsec) if __name__ == '__main__': try: ipsec = get_config() verify(ipsec) generate(ipsec) apply(ipsec) except ConfigError as e: print(e) exit(1)