From 11b5636519b360074eb2877006f2d8d63d9f6610 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Mon, 14 Jun 2021 13:04:04 +0200 Subject: ipsec: T2816: T645: T3613: Migrated IPsec to swanctl, includes multiple selectors, and selectors with VTI. --- data/templates/ipsec/ike-esp.tmpl | 32 ----- data/templates/ipsec/ipsec.conf.tmpl | 104 ---------------- data/templates/ipsec/ipsec.secrets.tmpl | 19 --- data/templates/ipsec/swanctl.conf.tmpl | 106 +++++++++------- data/templates/ipsec/swanctl/peer.tmpl | 136 +++++++++++++++++++++ data/templates/ipsec/swanctl/profile.tmpl | 34 ++++++ interface-definitions/vpn_ipsec.xml.in | 40 +----- smoketest/scripts/cli/test_vpn_ipsec.py | 109 ++++++++++++++--- src/conf_mode/vpn_ipsec.py | 41 ++++--- src/conf_mode/vpn_rsa-keys.py | 6 +- .../dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook | 30 +++-- src/etc/ipsec.d/vti-up-down | 14 +++ src/op_mode/vpn_ike_sa.py | 4 +- src/op_mode/vpn_ipsec.py | 13 +- 14 files changed, 401 insertions(+), 287 deletions(-) delete mode 100644 data/templates/ipsec/ike-esp.tmpl create mode 100644 data/templates/ipsec/swanctl/peer.tmpl create mode 100644 data/templates/ipsec/swanctl/profile.tmpl diff --git a/data/templates/ipsec/ike-esp.tmpl b/data/templates/ipsec/ike-esp.tmpl deleted file mode 100644 index deeb8c80d..000000000 --- a/data/templates/ipsec/ike-esp.tmpl +++ /dev/null @@ -1,32 +0,0 @@ -{% macro conn(ike, ike_ciphers, esp, esp_ciphers) -%} -{% if ike %} -{% if "key_exchange" in ike %} - keyexchange = {{ ike.key_exchange }} -{% endif %} - ike = {{ ike_ciphers }} -{% if "lifetime" in ike %} - ikelifetime = {{ ike.lifetime }}s -{% endif %} - reauth = {{ ike.ikev2_reauth if "ikev2_reauth" in ike else "no" }} - closeaction = {{ ike.close_action if "close_action" in ike else "none" }} -{% if "dead_peer_detection" in ike %} - dpdaction = {{ ike.dead_peer_detection.action }} - dpdtimeout = {{ ike.dead_peer_detection.timeout }} - dpddelay = {{ ike.dead_peer_detection.interval }} -{% endif %} -{% if "key_exchange" in ike and ike.key_exchange == "ikev1" and "mode" in ike and ike.mode == "aggressive" %} - aggressive = yes -{% endif %} -{% if "key_exchange" in ike and ike.key_exchange == "ikev2" %} - mobike = {{ "yes" if "mobike" not in ike or ike.mobike == "enable" else "no" }} -{% endif %} -{% endif %} -{% if esp %} - esp = {{ esp_ciphers }} -{% if "lifetime" in esp %} - keylife = {{ esp.lifetime }}s -{% endif %} - compress = {{ 'yes' if "compression" in esp and esp.compression == 'enable' else 'no' }} - type = {{ esp.mode if "mode" in esp else "tunnel" }} -{% endif %} -{%- endmacro %} diff --git a/data/templates/ipsec/ipsec.conf.tmpl b/data/templates/ipsec/ipsec.conf.tmpl index 18f6c0988..6550ea419 100644 --- a/data/templates/ipsec/ipsec.conf.tmpl +++ b/data/templates/ipsec/ipsec.conf.tmpl @@ -1,113 +1,9 @@ # Created by VyOS - manual changes will be overwritten -{% import 'ipsec/ike-esp.tmpl' as ike_esp %} - config setup charondebug = "{{ charondebug }}" uniqueids = {{ "no" if disable_uniqreqids is defined else "yes" }} -{% 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 %} -{% set peer_index = loop.index %} -{% set peer_ike = ike_group[peer_conf.ike_group] %} -{% set peer_esp = esp_group[peer_conf.default_esp_group] if peer_conf.default_esp_group is defined else None %} -conn peer-{{ peer }} -{% if peer_conf.authentication.mode in authby %} - authby = {{ authby[peer_conf.authentication.mode] }} -{% endif %} -{% if peer_conf.authentication.mode == 'x509' %} -{% set cert_file = peer_conf.authentication.x509.cert_file %} - leftcert = {{ cert_file if cert_file.startswith(x509_path) else (x509_path + cert_file) }} - leftsendcert = always - rightca = %same -{% elif peer_conf.authentication.mode == 'rsa' %} - leftsigkey = localhost.pub - rightsigkey = {{ peer_conf.authentication.rsa_key_name }}.pub -{% endif %} - left = {{ peer_conf.local_address if peer_conf.local_address != 'any' else '%defaultroute' }} # dhcp:{{ peer_conf.dhcp_interface if 'dhcp_interface' in peer_conf else 'no' }} -{% if peer_conf.authentication.id is defined and peer_conf.authentication.use_x509_id is not defined %} - leftid = "{{ peer_conf.authentication.id }}" -{% endif %} - right = {{ peer if peer not in ['any', '0.0.0.0'] and peer[0:1] != '@' else '%any' }} -{% if peer_conf.authentication.remote_id is defined %} - rightid = "{{ peer_conf.authentication.remote_id }}" -{% elif peer[0:1] == '@' %} - rightid = "{{ peer }}" -{% endif %} - keylife = 3600s - rekeymargin = 540s -{{ ike_esp.conn(peer_ike, ciphers.ike[peer_conf.ike_group], peer_esp, ciphers.esp[peer_conf.default_esp_group] if peer_esp else None) }} -{% if peer_conf.vti is defined and peer_conf.vti.bind is defined %} -{% set vti_esp = esp_group[peer_conf.vti.esp_group] if peer_conf.vti.esp_group is defined else None %} -conn peer-{{ peer }}-vti - also = peer-{{ peer }} - leftsubnet = 0.0.0.0/0 - leftupdown = "/etc/ipsec.d/vti-up-down {{ peer_conf.vti.bind }} {{ peer_conf.dhcp_interface if peer_conf.dhcp_interface is defined else 'no' }}" - rightsubnet = 0.0.0.0/0 - mark = {{ marks[peer_conf.vti.bind] }} -{{ ike_esp.conn(None, None, vti_esp, ciphers.esp[peer_conf.vti.esp_group] if vti_esp else None) }} -{% if peer[0:1] == '@' %} - rekey = no - auto = add - keyingtries = %forever -{% else %} -{% if peer_conf.connection_type is not defined or peer_conf.connection_type == 'initiate' %} - auto = start - keyingtries = %forever -{% elif peer_conf.connection_type == 'respond' %} - auto = route - keyingtries = 1 -{% endif %} -{% endif %} -{% elif peer_conf.tunnel is defined %} -{% for tunnel_id, tunnel_conf in peer_conf.tunnel.items() if tunnel_conf.disable is not defined %} -{% set tunnel_esp_name = tunnel_conf.esp_group if "esp_group" in tunnel_conf else peer_conf.default_esp_group %} -{% set tunnel_esp = esp_group[tunnel_esp_name] %} -{% set proto = tunnel_conf.protocol if "protocol" in tunnel_conf else '%any' %} -conn peer-{{ peer }}-tunnel-{{tunnel_id}} - also = peer-{{ peer }} -{% if tunnel_esp.mode is not defined or tunnel_esp.mode == 'tunnel' %} -{% if tunnel_conf.local is defined and tunnel_conf.local.prefix is defined %} - leftsubnet = {{ tunnel_conf.local.prefix if tunnel_conf.local.prefix != 'any' else '0.0.0.0/0' }}[{{ proto }}/{{ tunnel_conf.local.port if "port" in tunnel_conf.local else '%any' }}] -{% endif %} -{% if tunnel_conf.remote is defined and tunnel_conf.remote.prefix is defined %} - rightsubnet = {{ tunnel_conf.remote.prefix if tunnel_conf.remote.prefix != 'any' else '0.0.0.0/0' }}[{{ proto }}/{{ tunnel_conf.remote.port if "port" in tunnel_conf.remote else '%any' }}] -{% endif %} -{% elif tunnel_esp.mode == 'transport' %} - leftsubnet = {{ peer_conf.local_address }}[{{ proto }}/{{ tunnel_conf.local.port if "local" in tunnel_conf and "port" in tunnel_conf.local else '%any' }}] - rightsubnet = {{ peer }}[{{ proto }}/{{ tunnel_conf.local.port if "local" in tunnel_conf and "port" in tunnel_conf.local else '%any' }}] -{% endif %} -{% if tunnel_conf.esp_group is defined %} -{{ ike_esp.conn(None, None, tunnel_esp, ciphers.esp[tunnel_esp_name]) }} -{% endif %} -{% if peer[0:1] == '@' %} - rekey = no - auto = add - keyingtries = %forever -{% else %} -{% if peer_conf.connection_type is not defined or peer_conf.connection_type == 'initiate' %} - auto = start - keyingtries = %forever -{% elif peer_conf.connection_type == 'respond' %} - auto = route - keyingtries = 1 -{% endif %} -{% endif %} -{% if tunnel_conf.passthrough is defined and tunnel_conf.passthrough is not none %} -conn peer-{{ peer }}-tunnel-{{ tunnel_id }}-passthough - left = {{ peer_conf.local_address if peer_conf.local_address != 'any' else '%defaultroute' }} - right = {{ peer if peer not in ['any', '0.0.0.0'] and peer[0:1] != '@' else '%any' }} - leftsubnet = {{ tunnel_conf.local.prefix }} - rightsubnet = {{ tunnel_conf.local.prefix }} - type = passthrough - authby = never - auto = route -{% endif %} -{% endfor %} -{% endif %} -{% endfor %} -{% endif %} - {% if include_ipsec_conf is defined %} include {{ include_ipsec_conf }} {% endif %} diff --git a/data/templates/ipsec/ipsec.secrets.tmpl b/data/templates/ipsec/ipsec.secrets.tmpl index 0d2654abc..43b5fe0d2 100644 --- a/data/templates/ipsec/ipsec.secrets.tmpl +++ b/data/templates/ipsec/ipsec.secrets.tmpl @@ -1,24 +1,5 @@ # Created by VyOS - manual changes will be overwritten -{% if site_to_site is defined and "peer" in site_to_site %} -{% 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 %} -{% if peer_conf.authentication.mode == 'pre-shared-secret' %} -{{ (peer_conf.local_address if "local_address" in peer_conf else "%any") ~ - (" " ~ peer) ~ - ((" " ~ peer_conf.authentication.id) if "id" in peer_conf.authentication else "") ~ - ((" " ~ peer_conf.authentication.remote_id) if "remote_id" in peer_conf.authentication else "") -}} : PSK "{{ peer_conf.authentication.pre_shared_secret }}" # dhcp:{{ peer_conf.dhcp_interface if 'dhcp_interface' in peer_conf else 'no' }} -{% elif peer_conf.authentication.mode == 'x509' %} -{% set key_file = peer_conf.authentication.x509.key.file %} -: RSA {{ key_file if key_file.startswith(x509_path) else (x509_path + key_file) }}{% if "password" in peer_conf.authentication.x509.key and peer_conf.authentication.x509.key.password %} "{{ peer_conf.authentication.x509.key.password}}"{% endif %} -{% elif peer_conf.authentication.mode == 'rsa' and not ns.local_key_set %} -{% set ns.local_key_set = True %} -: RSA {{ rsa_local_key }} -{% endif %} -{% endfor %} -{% endif %} - {% if include_ipsec_secrets is defined %} include {{ include_ipsec_secrets }} {% endif %} diff --git a/data/templates/ipsec/swanctl.conf.tmpl b/data/templates/ipsec/swanctl.conf.tmpl index ce007c1fd..ea6d85743 100644 --- a/data/templates/ipsec/swanctl.conf.tmpl +++ b/data/templates/ipsec/swanctl.conf.tmpl @@ -1,54 +1,72 @@ # Created by VyOS - manual changes will be overwritten -{% if profile is defined %} +{% import 'ipsec/swanctl/profile.tmpl' as profile_tmpl %} +{% import 'ipsec/swanctl/peer.tmpl' as peer_tmpl %} + +{% if profile is defined or site_to_site is defined %} connections { -{% 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 %} -{% set dmvpn_ike = ike_group[profile_conf.ike_group] %} -{% set dmvpn_esp = esp_group[profile_conf.esp_group] %} -{% for interface in profile_conf.bind.tunnel %} - dmvpn-{{ name }}-{{ interface }} { - proposals = {{ ciphers.ike[profile_conf.ike_group][:-1] }} - version = {{ dmvpn_ike.key_exchange[4:] if "key_exchange" in dmvpn_ike else "0" }} - rekey_time = {{ dmvpn_ike.lifetime if 'lifetime' in dmvpn_ike else '28800' }}s - keyingtries = 0 -{% if profile_conf.authentication.mode == 'pre-shared-secret' %} - local { - auth = psk - } - remote { - auth = psk - } -{% endif %} - children { - dmvpn { - esp_proposals = {{ ciphers.esp[profile_conf.esp_group][:-1] }} - rekey_time = {{ dmvpn_esp.lifetime if 'lifetime' in dmvpn_esp else '3600' }}s - rand_time = 540s - local_ts = dynamic[gre] - remote_ts = dynamic[gre] - mode = {{ dmvpn_esp.mode if "mode" in dmvpn_esp else "transport" }} -{% if 'dead_peer_detection' in dmvpn_ike and 'action' in dmvpn_ike.dead_peer_detection %} - dpd_action = {{ dmvpn_ike.dead_peer_detection.action }} -{% endif %} -{% if 'compression' in dmvpn_esp and dmvpn_esp['compression'] == 'enable' %} - ipcomp = yes -{% endif %} - } - } - } -{% endfor %} -{% endfor %} +{% 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 %} +{% set dmvpn_ike = ike_group[profile_conf.ike_group] %} +{% set dmvpn_esp = esp_group[profile_conf.esp_group] %} +{{ profile_tmpl.conn(name, profile_conf, dmvpn_ike, dmvpn_esp, ciphers) }} +{% 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 %} +{% set peer_conn_name = peer.replace(".", "-").replace("@", "") %} +{% set peer_ike = ike_group[peer_conf.ike_group] %} +{% set peer_esp = esp_group[peer_conf.default_esp_group] if peer_conf.default_esp_group is defined else None %} +{% set auth_type = authby[peer_conf.authentication.mode] %} +{{ peer_tmpl.conn(peer_conn_name, peer, peer_conf, peer_ike, peer_esp, ciphers, esp_group, auth_type, marks) }} +{% endfor %} +{% endif %} } secrets { -{% 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 %} +{% 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 %} +{% 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_conn_name = peer.replace(".", "-").replace("@", "") %} +{% if peer_conf.authentication.mode == 'pre-shared-secret' %} + ike_{{ peer_conn_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_conn_name }} { + file = {{ peer_conf.authentication.x509.key.file }} +{% if "password" in peer_conf.authentication.x509.key and peer_conf.authentication.x509.key.password %} + secret = "{{ peer_conf.authentication.x509.key.password}}" +{% 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 %} } -{% endif %} +{% endif %} diff --git a/data/templates/ipsec/swanctl/peer.tmpl b/data/templates/ipsec/swanctl/peer.tmpl new file mode 100644 index 000000000..0d01cd546 --- /dev/null +++ b/data/templates/ipsec/swanctl/peer.tmpl @@ -0,0 +1,136 @@ +{% macro conn(name, peer, peer_conf, ike, esp, ciphers, esp_group, auth_type, marks) %} + peer_{{ name }} { + proposals = {{ ciphers.ike[peer_conf.ike_group] }} + version = {{ ike['key_exchange'][4:] if "key_exchange" in ike else "0" }} + local_addrs = {{ peer_conf.local_address if peer_conf.local_address != 'any' else '0.0.0.0/0' }} # dhcp:{{ peer_conf.dhcp_interface if 'dhcp_interface' in peer_conf else 'no' }} + remote_addrs = {{ peer if peer not in ['any', '0.0.0.0'] and peer[0:1] != '@' else '0.0.0.0/0' }} +{% if peer_conf.authentication.mode == 'x509' %} + send_cert = always +{% endif %} +{% if "dead_peer_detection" in ike %} + dpd_timeout = {{ ike.dead_peer_detection.timeout }} + dpd_delay = {{ ike.dead_peer_detection.interval }} +{% endif %} +{% if "key_exchange" in ike and ike.key_exchange == "ikev1" and "mode" in ike and ike.mode == "aggressive" %} + aggressive = yes +{% endif %} + mobike = {{ "yes" if "mobike" not in ike or ike.mobike == "enable" else "no" }} +{% if peer[0:1] == '@' %} + keyingtries = 0 + rekey_time = 0 + reauth_time = 0 +{% elif peer_conf.connection_type is not defined or peer_conf.connection_type == 'initiate' %} + keyingtries = 0 +{% elif peer_conf.connection_type is defined and peer_conf.connection_type == 'respond' %} + keyingtries = 1 +{% endif %} +{% if peer_conf.force_encapsulation is defined and peer_conf.force_encapsulation == 'enable' %} + encap = yes +{% endif %} + local { +{% if peer_conf.authentication.id is defined and peer_conf.authentication.use_x509_id is not defined %} + id = "{{ peer_conf.authentication.id }}" +{% endif %} +{% if auth_type %} + auth = {{ auth_type }} +{% endif %} +{% if peer_conf.authentication.mode == 'x509' %} + certs = {{ peer_conf.authentication.x509.cert_file }} +{% elif peer_conf.authentication.mode == 'rsa' %} + pubkeys = localhost.pub +{% endif %} + } + remote { +{% if peer_conf.authentication.remote_id is defined %} + id = "{{ peer_conf.authentication.remote_id }}" +{% elif peer[0:1] == '@' %} + id = "{{ peer }}" +{% endif %} +{% if auth_type %} + auth = {{ auth_type }} +{% endif %} +{% if peer_conf.authentication.mode == 'rsa' %} + pubkeys = {{ peer_conf.authentication.rsa_key_name }}.pub +{% endif %} + } + children { +{% if peer_conf.vti is defined and peer_conf.vti.bind is defined and peer_conf.tunnel is not defined %} +{% set vti_esp = esp_group[peer_conf.vti.esp_group] if peer_conf.vti.esp_group is defined else None %} + peer_{{ name }}_vti { + esp_proposals = {{ ciphers.esp[peer_conf.vti.esp_group] }} + local_ts = 0.0.0.0/0,::/0 + remote_ts = 0.0.0.0/0,::/0 + updown = "/etc/ipsec.d/vti-up-down {{ peer_conf.vti.bind }} {{ peer_conf.dhcp_interface if peer_conf.dhcp_interface is defined else 'no' }}" + mark_in = {{ marks[peer_conf.vti.bind] }} + mark_out = {{ marks[peer_conf.vti.bind] }} + ipcomp = {{ 'yes' if "compression" in vti_esp and vti_esp.compression == 'enable' else 'no' }} + mode = {{ vti_esp.mode if "mode" in vti_esp else "tunnel" }} +{% if peer[0:1] == '@' %} + start_action = none +{% elif peer_conf.connection_type is not defined or peer_conf.connection_type == 'initiate' %} + start_action = start +{% elif peer_conf.connection_type == 'respond' %} + start_action = trap +{% endif %} +{% if "dead_peer_detection" in ike %} +{% set dpd_translate = {'clear': 'clear', 'hold': 'trap', 'restart': 'start'} %} + dpd_action = {{ dpd_translate[ike.dead_peer_detection.action] }} +{% endif %} + } +{% endif %} +{% if peer_conf.tunnel is defined %} +{% for tunnel_id, tunnel_conf in peer_conf.tunnel.items() if tunnel_conf.disable is not defined %} +{% set tunnel_esp_name = tunnel_conf.esp_group if "esp_group" in tunnel_conf else peer_conf.default_esp_group %} +{% set tunnel_esp = esp_group[tunnel_esp_name] %} +{% set proto = tunnel_conf.protocol if "protocol" in tunnel_conf else '' %} +{% set local_port = tunnel_conf.local.port if tunnel_conf.local is defined and tunnel_conf.local.port is defined else '' %} +{% set local_suffix = '[{0}/{1}]'.format(proto, local_port) if proto or local_port else '' %} +{% set remote_port = tunnel_conf.remote.port if tunnel_conf.remote is defined and tunnel_conf.remote.port is defined else '' %} +{% set remote_suffix = '[{0}/{1}]'.format(proto, remote_port) if proto or remote_port else '' %} + peer_{{ name }}_tunnel_{{ tunnel_id }} { + esp_proposals = {{ ciphers.esp[tunnel_esp_name] }} +{% if tunnel_esp.mode is not defined or tunnel_esp.mode == 'tunnel' %} +{% if tunnel_conf.local is defined and tunnel_conf.local.prefix is defined %} +{% set local_prefix = tunnel_conf.local.prefix if 'any' not in tunnel_conf.local.prefix else ['0.0.0.0/0', '::/0'] %} + local_ts = {{ local_prefix | join(local_suffix + ",") }}{{ local_suffix }} +{% endif %} +{% if tunnel_conf.remote is defined and tunnel_conf.remote.prefix is defined %} +{% set remote_prefix = tunnel_conf.remote.prefix if 'any' not in tunnel_conf.remote.prefix else ['0.0.0.0/0', '::/0'] %} + remote_ts = {{ remote_prefix | join(remote_suffix + ",") }}{{ remote_suffix }} +{% endif %} +{% elif tunnel_esp.mode == 'transport' %} + local_ts = {{ peer_conf.local_address }}{{ local_suffix }} + remote_ts = {{ peer }}{{ remote_suffix }} +{% endif %} + ipcomp = {{ 'yes' if "compression" in tunnel_esp and tunnel_esp.compression == 'enable' else 'no' }} + mode = {{ tunnel_esp.mode if "mode" in tunnel_esp else "tunnel" }} +{% if peer[0:1] == '@' %} + start_action = none +{% elif peer_conf.connection_type is not defined or peer_conf.connection_type == 'initiate' %} + start_action = start +{% elif peer_conf.connection_type == 'respond' %} + start_action = trap +{% endif %} +{% if "dead_peer_detection" in ike %} +{% set dpd_translate = {'clear': 'clear', 'hold': 'trap', 'restart': 'start'} %} + dpd_action = {{ dpd_translate[ike.dead_peer_detection.action] }} +{% endif %} +{% if peer_conf.vti is defined and peer_conf.vti.bind is defined %} + updown = "/etc/ipsec.d/vti-up-down {{ peer_conf.vti.bind }} {{ peer_conf.dhcp_interface if peer_conf.dhcp_interface is defined else 'no' }}" + mark_in = {{ marks[peer_conf.vti.bind] }} + mark_out = {{ marks[peer_conf.vti.bind] }} +{% endif %} + } +{% if tunnel_conf.passthrough is defined and tunnel_conf.passthrough %} + peer_{{ name }}_tunnel_{{ tunnel_id }}_passthough { + local_ts = {{ tunnel_conf.passthrough | join(",") }} + remote_ts = {{ tunnel_conf.passthrough | join(",") }} + start_action = trap + mode = pass + } +{% endif %} +{% endfor %} +{% endif %} + } + } +{% endmacro %} diff --git a/data/templates/ipsec/swanctl/profile.tmpl b/data/templates/ipsec/swanctl/profile.tmpl new file mode 100644 index 000000000..e4b36b99f --- /dev/null +++ b/data/templates/ipsec/swanctl/profile.tmpl @@ -0,0 +1,34 @@ +{% macro conn(name, profile_conf, ike, esp, ciphers) %} +{% for interface in profile_conf.bind.tunnel %} + dmvpn-{{ name }}-{{ interface }} { + proposals = {{ ciphers.ike[profile_conf.ike_group] }} + version = {{ ike.key_exchange[4:] if "key_exchange" in ike else "0" }} + rekey_time = {{ ike.lifetime if 'lifetime' in ike else '28800' }}s + keyingtries = 0 +{% if profile_conf.authentication.mode == 'pre-shared-secret' %} + local { + auth = psk + } + remote { + auth = psk + } +{% endif %} + children { + dmvpn { + esp_proposals = {{ ciphers.esp[profile_conf.esp_group] }} + rekey_time = {{ esp.lifetime if 'lifetime' in esp else '3600' }}s + rand_time = 540s + local_ts = dynamic[gre] + remote_ts = dynamic[gre] + mode = {{ esp.mode if "mode" in esp else "transport" }} +{% if 'dead_peer_detection' in ike and 'action' in ike.dead_peer_detection %} + dpd_action = {{ ike.dead_peer_detection.action }} +{% endif %} +{% if 'compression' in esp and esp['compression'] == 'enable' %} + ipcomp = yes +{% endif %} + } + } + } +{% endfor %} +{% endmacro %} diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index d7435d6df..2031217ba 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -969,44 +969,6 @@ - - - Option to allow NAT networks - - enable disable - - - enable - Enable NAT networks - - - disable - Disable NAT networks (default) - - - ^(enable|disable)$ - - - - - - Option to allow public networks - - enable disable - - - enable - Enable public networks - - - disable - Disable public networks (default) - - - ^(enable|disable)$ - - - #include @@ -1049,6 +1011,7 @@ + @@ -1087,6 +1050,7 @@ + diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 627d73d5c..b27ed3ca5 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -23,16 +23,19 @@ from vyos.util import call, process_named_running, read_file ethernet_path = ['interfaces', 'ethernet'] tunnel_path = ['interfaces', 'tunnel'] +vti_path = ['interfaces', 'vti'] nhrp_path = ['protocols', 'nhrp'] base_path = ['vpn', 'ipsec'] dhcp_waiting_file = '/tmp/ipsec_dhcp_waiting' +swanctl_file = '/etc/swanctl/swanctl.conf' class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_delete(nhrp_path) self.cli_delete(tunnel_path) + self.cli_delete(vti_path) self.cli_delete(ethernet_path) self.cli_commit() @@ -77,6 +80,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ["ike-group", "MyIKEGroup", "proposal", "1", "dh-group", "2"]) self.cli_set(base_path + ["ike-group", "MyIKEGroup", "proposal", "1", "encryption", "aes128"]) self.cli_set(base_path + ["ike-group", "MyIKEGroup", "proposal", "1", "hash", "sha1"]) + self.cli_set(base_path + ["ike-group", "MyIKEGroup", "key-exchange", "ikev2"]) # Site to site self.cli_set(base_path + ["ipsec-interfaces", "interface", "eth0"]) @@ -85,33 +89,104 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "ike-group", "MyIKEGroup"]) self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "default-esp-group", "MyESPGroup"]) self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "local-address", "192.0.2.10"]) - self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "protocol", "gre"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "protocol", "tcp"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "local", "prefix", "172.16.10.0/24"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "local", "prefix", "172.16.11.0/24"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "local", "port", "443"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "remote", "prefix", "172.17.10.0/24"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "remote", "prefix", "172.17.11.0/24"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "remote", "port", "443"]) self.cli_commit() - ipsec_conf_lines = [ - 'authby = secret', - 'ike = aes128-sha1-modp1024!', - 'esp = aes128-sha1-modp1024!', - 'left = 192.0.2.10', - 'right = 203.0.113.45', - 'type = tunnel' + swanctl_conf_lines = [ + 'version = 2', + 'auth = psk', + 'proposals = aes128-sha1-modp1024', + 'esp_proposals = aes128-sha1-modp1024', + 'local_addrs = 192.0.2.10 # dhcp:no', + 'remote_addrs = 203.0.113.45', + 'mode = tunnel', + 'local_ts = 172.16.10.0/24[tcp/443],172.16.11.0/24[tcp/443]', + 'remote_ts = 172.17.10.0/24[tcp/443],172.17.11.0/24[tcp/443]' + ] + + swanctl_secrets_lines = [ + 'id-local = 192.0.2.10 # dhcp:no', + 'id-remote = 203.0.113.45', + 'secret = "MYSECRETKEY"' ] - ipsec_secrets_lines = [ - '192.0.2.10 203.0.113.45 : PSK "MYSECRETKEY" # dhcp:no' + tmp_swanctl_conf = read_file(swanctl_file) + + for line in swanctl_conf_lines: + self.assertIn(line, tmp_swanctl_conf) + + for line in swanctl_secrets_lines: + self.assertIn(line, tmp_swanctl_conf) + + # Check for running process + self.assertTrue(process_named_running('charon')) + + def test_site_to_site_vti(self): + self.cli_delete(base_path) + self.cli_delete(vti_path) + + # VTI interface + self.cli_set(vti_path + ["vti10", "address", "10.1.1.1/24"]) + + # IKE/ESP Groups + self.cli_set(base_path + ["esp-group", "MyESPGroup", "proposal", "1", "encryption", "aes128"]) + self.cli_set(base_path + ["esp-group", "MyESPGroup", "proposal", "1", "hash", "sha1"]) + self.cli_set(base_path + ["ike-group", "MyIKEGroup", "proposal", "1", "dh-group", "2"]) + self.cli_set(base_path + ["ike-group", "MyIKEGroup", "proposal", "1", "encryption", "aes128"]) + self.cli_set(base_path + ["ike-group", "MyIKEGroup", "proposal", "1", "hash", "sha1"]) + self.cli_set(base_path + ["ike-group", "MyIKEGroup", "key-exchange", "ikev2"]) + + # Site to site + self.cli_set(base_path + ["ipsec-interfaces", "interface", "eth0"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "authentication", "mode", "pre-shared-secret"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "authentication", "pre-shared-secret", "MYSECRETKEY"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "ike-group", "MyIKEGroup"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "default-esp-group", "MyESPGroup"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "local-address", "192.0.2.10"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "local", "prefix", "172.16.10.0/24"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "local", "prefix", "172.16.11.0/24"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "remote", "prefix", "172.17.10.0/24"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "tunnel", "1", "remote", "prefix", "172.17.11.0/24"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "vti", "bind", "vti10"]) + self.cli_set(base_path + ["site-to-site", "peer", "203.0.113.45", "vti", "esp-group", "MyESPGroup"]) + + self.cli_commit() + + swanctl_conf_lines = [ + 'version = 2', + 'auth = psk', + 'proposals = aes128-sha1-modp1024', + 'esp_proposals = aes128-sha1-modp1024', + 'local_addrs = 192.0.2.10 # dhcp:no', + 'remote_addrs = 203.0.113.45', + 'mode = tunnel', + 'local_ts = 172.16.10.0/24,172.16.11.0/24', + 'remote_ts = 172.17.10.0/24,172.17.11.0/24', + 'mark_in = 9437194', # 0x900000 + (vti)10 + 'mark_out = 9437194', + 'updown = "/etc/ipsec.d/vti-up-down vti10 no"' ] - tmp_ipsec_conf = read_file('/etc/ipsec.conf') + swanctl_secrets_lines = [ + 'id-local = 192.0.2.10 # dhcp:no', + 'id-remote = 203.0.113.45', + 'secret = "MYSECRETKEY"' + ] - for line in ipsec_conf_lines: - self.assertIn(line, tmp_ipsec_conf) + tmp_swanctl_conf = read_file(swanctl_file) - call('sudo chmod 644 /etc/ipsec.secrets') # Needs to be readable - tmp_ipsec_secrets = read_file('/etc/ipsec.secrets') + for line in swanctl_conf_lines: + self.assertIn(line, tmp_swanctl_conf) - for line in ipsec_secrets_lines: - self.assertIn(line, tmp_ipsec_secrets) + for line in swanctl_secrets_lines: + self.assertIn(line, tmp_swanctl_conf) # Check for running process self.assertTrue(process_named_running('charon')) diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index f80a9455a..433c51e7e 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -28,7 +28,6 @@ from vyos.template import render from vyos.validate import is_ipv6_link_local from vyos.util import call from vyos.util import dict_search -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 @@ -37,9 +36,9 @@ from vyos import airbag airbag.enable() authby_translate = { - 'pre-shared-secret': 'secret', - 'rsa': 'rsasig', - 'x509': 'rsasig' + 'pre-shared-secret': 'psk', + 'rsa': 'pubkey', + 'x509': 'pubkey' } default_pfs = 'dh-group2' pfs_translate = { @@ -80,8 +79,10 @@ dhcp_wait_sleep = 1 mark_base = 0x900000 -CA_PATH = "/etc/ipsec.d/cacerts/" -CRL_PATH = "/etc/ipsec.d/crls/" +CERT_PATH="/etc/swanctl/x509/" +KEY_PATH="/etc/swanctl/private/" +CA_PATH = "/etc/swanctl/x509ca/" +CRL_PATH = "/etc/swanctl/x509crl/" DHCP_BASE = "/var/lib/dhcp/dhclient" DHCP_HOOK_IFLIST="/tmp/ipsec_dhcp_waiting" @@ -126,7 +127,7 @@ def get_config(config=None): if enc and hash: ciphers.append(f"{enc}-{hash}-{pfs_translate[pfs]}" if pfs else f"{enc}-{hash}") - ike_ciphers[group] = ','.join(ciphers) + '!' + ike_ciphers[group] = ','.join(ciphers) if 'esp_group' in ipsec: for group, esp_conf in ipsec['esp_group'].items(): @@ -146,7 +147,7 @@ def get_config(config=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) + '!' + esp_ciphers[group] = ','.join(ciphers) return ipsec @@ -324,7 +325,6 @@ def generate(ipsec): data['ciphers'] = {'ike': ike_ciphers, 'esp': esp_ciphers} data['marks'] = {} data['rsa_local_key'] = verify_rsa_local_key(ipsec) - 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(): @@ -332,6 +332,12 @@ def generate(ipsec): continue if peer_conf['authentication']['mode'] == 'x509': + cert_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['cert_file']) + call(f'cp -f {cert_file} {CERT_PATH}') + + key_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['key']['file']) + call(f'cp -f {key_file} {KEY_PATH}') + ca_cert_file = os.path.join(X509_PATH, peer_conf['authentication']['x509']['ca_cert_file']) call(f'cp -f {ca_cert_file} {CA_PATH}') @@ -350,15 +356,22 @@ def generate(ipsec): 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: + + if 'tunnel' in peer_conf: for tunnel, tunnel_conf in peer_conf['tunnel'].items(): - local_prefix = dict_search('local.prefix', tunnel_conf) - remote_prefix = dict_search('remote.prefix', tunnel_conf) + local_prefixes = dict_search('local.prefix', tunnel_conf) + remote_prefixes = dict_search('remote.prefix', tunnel_conf) - if not local_prefix or not remote_prefix: + if not local_prefixes or not remote_prefixes: continue - passthrough = cidr_fit(local_prefix, remote_prefix) + passthrough = [] + + for local_prefix in local_prefixes: + for remote_prefix in remote_prefixes: + if cidr_fit(local_prefix, remote_prefix): + passthrough.append(local_prefix) + data['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough if 'logging' in ipsec and 'log_modes' in ipsec['logging']: diff --git a/src/conf_mode/vpn_rsa-keys.py b/src/conf_mode/vpn_rsa-keys.py index 6cf7eba6e..c6ff369ad 100755 --- a/src/conf_mode/vpn_rsa-keys.py +++ b/src/conf_mode/vpn_rsa-keys.py @@ -29,7 +29,8 @@ from Crypto.PublicKey.RSA import construct airbag.enable() LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/'] -LOCAL_OUTPUT = '/etc/ipsec.d/certs/localhost.pub' +LOCAL_OUTPUT = '/etc/swanctl/pubkey/localhost.pub' +LOCAL_KEY_OUTPUT = '/etc/swanctl/private/localhost.key' def get_config(config=None): if config: @@ -68,6 +69,7 @@ def generate(conf): if 'local_key' in conf and 'file' in conf['local_key']: local_key = conf['local_key']['file'] local_key_path = get_local_key(local_key) + call(f'sudo cp -f {local_key_path} {LOCAL_KEY_OUTPUT}') call(f'sudo /usr/bin/openssl rsa -in {local_key_path} -pubout -out {LOCAL_OUTPUT}') if 'rsa_key_name' in conf: @@ -82,7 +84,7 @@ def generate(conf): else: remote_key = bytes('-----BEGIN PUBLIC KEY-----\n' + remote_key + '\n-----END PUBLIC KEY-----\n', 'utf-8') - with open(f'/etc/ipsec.d/certs/{key_name}.pub', 'wb') as f: + with open(f'/etc/swanctl/pubkey/{key_name}.pub', 'wb') as f: f.write(remote_key) def migrate_from_vyatta_key(data): diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook index e00e5fe6e..a7a9a2ce6 100755 --- a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook @@ -1,4 +1,18 @@ #!/bin/bash +# +# 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 . if [ "$reason" == "REBOOT" ] || [ "$reason" == "EXPIRE" ]; then exit 0 @@ -24,8 +38,7 @@ import re from vyos.util import call from vyos.util import cmd -IPSEC_CONF="/etc/ipsec.conf" -IPSEC_SECRETS="/etc/ipsec.secrets" +SWANCTL_CONF="/etc/swanctl/swanctl.conf" def getlines(file): with open(file, 'r') as f: @@ -41,7 +54,7 @@ def ipsec_down(ip_address): connection_name = None for line in status.split("\n"): if line.find(ip_address) > 0: - regex_match = re.search(r'(peer-[^:\[]+)', line) + regex_match = re.search(r'(peer_[^:\[]+)', line) if regex_match: connection_name = regex_match[1] break @@ -53,8 +66,7 @@ if __name__ == '__main__': new_ip = os.getenv('new_ip_address') old_ip = os.getenv('old_ip_address') - conf_lines = getlines(IPSEC_CONF) - secrets_lines = getlines(IPSEC_SECRETS) + conf_lines = getlines(SWANCTL_CONF) found = False to_match = f'# dhcp:{interface}' @@ -68,9 +80,9 @@ if __name__ == '__main__': secrets_lines[i] = line.replace(old_ip, new_ip) if found: - writelines(IPSEC_CONF, conf_lines) - writelines(IPSEC_SECRETS, secrets_lines) + writelines(SWANCTL_CONF, conf_lines) ipsec_down(old_ip) - call('sudo /usr/sbin/ipsec rereadall') - call('sudo /usr/sbin/ipsec reload') + call('sudo ipsec rereadall') + call('sudo ipsec reload') + call('sudo swanctl -q') PYEND \ No newline at end of file diff --git a/src/etc/ipsec.d/vti-up-down b/src/etc/ipsec.d/vti-up-down index 0e1cd7753..2b66dd9e6 100755 --- a/src/etc/ipsec.d/vti-up-down +++ b/src/etc/ipsec.d/vti-up-down @@ -1,4 +1,18 @@ #!/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 . ## Script called up strongswan to bring the vti interface up/down based on the state of the IPSec tunnel. ## Called as vti_up_down vti_intf_name diff --git a/src/op_mode/vpn_ike_sa.py b/src/op_mode/vpn_ike_sa.py index 28da9f8dc..fe016da45 100755 --- a/src/op_mode/vpn_ike_sa.py +++ b/src/op_mode/vpn_ike_sa.py @@ -36,9 +36,9 @@ def ike_sa(peer, nat): peers = [] for conn in sas: for name, sa in conn.items(): - if peer and not name.startswith('peer-' + peer): + if peer and not name.startswith('peer_' + peer): continue - if name.startswith('peer-') and name in peers: + if name.startswith('peer_') and name in peers: continue if nat and 'nat-local' not in sa: continue diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py index 434186abb..582b5ef95 100755 --- a/src/op_mode/vpn_ipsec.py +++ b/src/op_mode/vpn_ipsec.py @@ -100,13 +100,13 @@ def generate_x509_pair(name): print(f'Private key: {X509_PATH}{name}.key') def get_peer_connections(peer, tunnel, return_all = False): - search = rf'^conn (peer-{peer}-(tunnel-[\d]+|vti))$' + search = rf'^[\s]*(peer_{peer}_(tunnel_[\d]+|vti)).*' matches = [] - with open(IPSEC_CONF, 'r') as f: + with open(SWANCTL_CONF, 'r') as f: for line in f.readlines(): result = re.match(search, line) if result: - suffix = f'tunnel-{tunnel}' if tunnel.isnumeric() else tunnel + suffix = f'tunnel_{tunnel}' if tunnel.isnumeric() else tunnel if return_all or (result[2] == suffix): matches.append(result[1]) return matches @@ -171,13 +171,14 @@ def debug_peer(peer, tunnel): if not tunnel or tunnel == 'all': tunnel = '' - conn = get_peer_connection(peer, tunnel) + conn = get_peer_connections(peer, tunnel) - if not conn: + if not conns: print('Peer not found, aborting') return - call(f'sudo /usr/sbin/ipsec statusall | grep {conn}') + for conn in conns: + call(f'sudo /usr/sbin/ipsec statusall | grep {conn}') if __name__ == '__main__': parser = argparse.ArgumentParser() -- cgit v1.2.3