diff options
-rw-r--r-- | data/templates/ipsec/swanctl.conf.tmpl | 32 | ||||
-rw-r--r-- | data/templates/ipsec/swanctl/remote_access.tmpl | 40 | ||||
-rw-r--r-- | debian/control | 1 | ||||
-rw-r--r-- | interface-definitions/include/ipsec/local-address.xml.i | 27 | ||||
-rw-r--r-- | interface-definitions/include/ipsec/local-traffic-selector.xml.i | 28 | ||||
-rw-r--r-- | interface-definitions/vpn_ipsec.xml.in | 188 | ||||
-rw-r--r-- | python/vyos/util.py | 33 | ||||
-rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 42 |
8 files changed, 292 insertions, 99 deletions
diff --git a/data/templates/ipsec/swanctl.conf.tmpl b/data/templates/ipsec/swanctl.conf.tmpl index cafe52e78..0eda8479a 100644 --- a/data/templates/ipsec/swanctl.conf.tmpl +++ b/data/templates/ipsec/swanctl.conf.tmpl @@ -1,6 +1,7 @@ ### 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 %} @@ -13,6 +14,25 @@ connections { {{ 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 %} + 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 { @@ -60,5 +80,17 @@ secrets { {% 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 %} +{% 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 new file mode 100644 index 000000000..a3a1cf0b2 --- /dev/null +++ b/data/templates/ipsec/swanctl/remote_access.tmpl @@ -0,0 +1,40 @@ +{% 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 + pools = ra-{{ name }} + 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 %} + 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 %} + certs = {{ rw_conf.authentication.x509.certificate }}.pem +{% endif %} + } + remote { + auth = eap-mschapv2 + eap_id = %any + } + 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/debian/control b/debian/control index aed1a3b2d..5a434bfc6 100644 --- a/debian/control +++ b/debian/control @@ -24,6 +24,7 @@ Build-Depends: python3-setuptools, python3-sphinx, python3-xmltodict, + python3-inotify, quilt, whois Standards-Version: 3.9.6 diff --git a/interface-definitions/include/ipsec/local-address.xml.i b/interface-definitions/include/ipsec/local-address.xml.i new file mode 100644 index 000000000..2de6ecb1f --- /dev/null +++ b/interface-definitions/include/ipsec/local-address.xml.i @@ -0,0 +1,27 @@ +<!-- include start from ipsec/local-address.xml.i --> +<leafNode name="local-address"> + <properties> + <help>IPv4 or IPv6 address of a local interface to use for VPN</help> + <completionHelp> + <list>any</list> + </completionHelp> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address of a local interface for VPN</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of a local interface for VPN</description> + </valueHelp> + <valueHelp> + <format>any</format> + <description>Allow any IPv4 address present on the system to be used for VPN</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + <regex>^(any)$</regex> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/ipsec/local-traffic-selector.xml.i b/interface-definitions/include/ipsec/local-traffic-selector.xml.i new file mode 100644 index 000000000..d30a6d11a --- /dev/null +++ b/interface-definitions/include/ipsec/local-traffic-selector.xml.i @@ -0,0 +1,28 @@ +<!-- include start from ipsec/local-traffic-selector.xml.i --> +<node name="local"> + <properties> + <help>Local parameters for interesting traffic</help> + </properties> + <children> + #include <include/port-number.xml.i> + <leafNode name="prefix"> + <properties> + <help>Local IPv4 or IPv6 prefix</help> + <valueHelp> + <format>ipv4</format> + <description>Local IPv4 prefix</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Local IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index ff60bb82f..f6b18d1d5 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -692,9 +692,12 @@ <leafNode name="tunnel"> <properties> <help>Tunnel interface associated with this configuration profile</help> + <completionHelp> + <path>interfaces tunnel</path> + </completionHelp> <valueHelp> <format>txt</format> - <description>Tunnel interface associated with this configuration profile</description> + <description>Associated interface to this configuration profile</description> </valueHelp> <multi/> </properties> @@ -705,6 +708,105 @@ #include <include/ipsec/ike-group.xml.i> </children> </tagNode> + <tagNode name="remote-access"> + <properties> + <help>Remote access IKEv2 VPN </help> + </properties> + <children> + <node name="authentication"> + <properties> + <help>Authentication for remote access</help> + </properties> + <children> + #include <include/ipsec/authentication-id.xml.i> + #include <include/ipsec/authentication-x509.xml.i> + <node name="local-users"> + <properties> + <help>Local user authentication for PPPoE server</help> + </properties> + <children> + <tagNode name="username"> + <properties> + <help>User name for authentication</help> + </properties> + <children> + #include <include/generic-disable-node.xml.i> + <leafNode name="password"> + <properties> + <help>Password for authentication</help> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + #include <include/generic-description.xml.i> + #include <include/generic-disable-node.xml.i> + #include <include/ipsec/esp-group.xml.i> + #include <include/ipsec/ike-group.xml.i> + #include <include/ipsec/local-address.xml.i> + #include <include/ipsec/local-traffic-selector.xml.i> + <node name="pool"> + <properties> + <help>IP address pool for remote-access users</help> + </properties> + <children> + <leafNode name="exclude"> + <properties> + <help>Local IPv4 or IPv6 pool prefix exclusions</help> + <valueHelp> + <format>ipv4</format> + <description>Local IPv4 pool prefix exclusion</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Local IPv6 pool prefix exclusion</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="prefix"> + <properties> + <help>Local IPv4 or IPv6 pool prefix</help> + <valueHelp> + <format>ipv4</format> + <description>Local IPv4 pool prefix</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Local IPv6 pool prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + <!-- Include Accel-PPP definition here, maybe time for a rename? --> + #include <include/accel-ppp/name-server.xml.i> + </children> + </node> + <leafNode name="timeout"> + <properties> + <help>Timeout to close connection if no data is transmitted</help> + <valueHelp> + <format>u32:10-86400</format> + <description>Timeout in seconds (default 28800)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 10-86400"/> + </constraint> + </properties> + <defaultValue>28800</defaultValue> + </leafNode> + </children> + </tagNode> <node name="site-to-site"> <properties> <help>Site-to-site VPN</help> @@ -872,31 +974,7 @@ </constraint> </properties> </leafNode> - <leafNode name="local-address"> - <properties> - <help>IPv4 or IPv6 address of a local interface to use for VPN</help> - <completionHelp> - <list>any</list> - </completionHelp> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address of a local interface for VPN</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>IPv6 address of a local interface for VPN</description> - </valueHelp> - <valueHelp> - <format>any</format> - <description>Allow any IPv4 address present on the system to be used for VPN</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - <validator name="ipv6-address"/> - <regex>^(any)$</regex> - </constraint> - </properties> - </leafNode> + #include <include/ipsec/local-address.xml.i> <tagNode name="tunnel"> <properties> <help>Peer tunnel [REQUIRED]</help> @@ -908,63 +986,14 @@ <children> #include <include/generic-disable-node.xml.i> #include <include/ipsec/esp-group.xml.i> - <node name="local"> - <properties> - <help>Local parameters for interesting traffic</help> - </properties> - <children> - <leafNode name="port"> - <properties> - <help>Any TCP or UDP port</help> - <valueHelp> - <format>port name</format> - <description>Named port (any name in /etc/services, e.g., http)</description> - </valueHelp> - <valueHelp> - <format>u32:1-65535</format> - <description>Numbered port</description> - </valueHelp> - </properties> - </leafNode> - <leafNode name="prefix"> - <properties> - <help>Local IPv4 or IPv6 prefix</help> - <valueHelp> - <format>ipv4</format> - <description>Local IPv4 prefix</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>Local IPv6 prefix</description> - </valueHelp> - <constraint> - <validator name="ipv4-prefix"/> - <validator name="ipv6-prefix"/> - </constraint> - <multi/> - </properties> - </leafNode> - </children> - </node> + #include <include/ipsec/local-traffic-selector.xml.i> #include <include/ip-protocol.xml.i> <node name="remote"> <properties> <help>Remote parameters for interesting traffic</help> </properties> <children> - <leafNode name="port"> - <properties> - <help>Any TCP or UDP port</help> - <valueHelp> - <format>port name</format> - <description>Named port (any name in /etc/services, e.g., http)</description> - </valueHelp> - <valueHelp> - <format>u32:1-65535</format> - <description>Numbered port</description> - </valueHelp> - </properties> - </leafNode> + #include <include/port-number.xml.i> <leafNode name="prefix"> <properties> <help>Remote IPv4 or IPv6 prefix</help> @@ -994,7 +1023,10 @@ <children> <leafNode name="bind"> <properties> - <help>VTI tunnel interface associated with this configuration [REQUIRED]</help> + <help>VTI tunnel interface associated with this configuration</help> + <completionHelp> + <path>interfaces vti</path> + </completionHelp> </properties> </leafNode> #include <include/ipsec/esp-group.xml.i> diff --git a/python/vyos/util.py b/python/vyos/util.py index cf90dc74f..8247ccb2d 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -506,6 +506,39 @@ def file_is_persistent(path): absolute = os.path.abspath(os.path.dirname(path)) return re.match(location,absolute) +def wait_for_inotify(file_path, event_type=None, timeout=None, sleep_interval=0.1): + """ Waits for an inotify event to occur """ + if not os.path.dirname(file_path): + raise ValueError( + "File path {} does not have a directory part (required for inotify watching)".format(file_path)) + if not os.path.basename(file_path): + raise ValueError( + "File path {} does not have a file part, do not know what to watch for".format(file_path)) + + from inotify.adapters import Inotify + from time import time + + time_start = time() + + i = Inotify() + i.add_watch(os.path.dirname(file_path)) + + for event in i.event_gen(yield_nones=True): + if (timeout is not None) and ((time.time() - time_start) > timeout): + # If the function didn't return until this point, + # the file failed to have been written to and closed within the timeout + raise OSError("Waiting for file {} to be written has failed".format(file_path)) + + if event is not None: + (_, type_names, path, filename) = event + if filename == os.path.basename(file_path): + if event_type in type_names: + return + +def wait_for_file_write_complete(file_path, timeout=None, sleep_interval=0.1): + """ Waits for a process to close a file after opening it in write mode. """ + wait_for_inotify(file_path, + event_type='IN_CLOSE_WRITE', timeout=timeout, sleep_interval=sleep_interval) def commit_in_progress(): """ Not to be used in normal op mode scripts! """ diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index d1b29ee9a..50223320d 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -34,7 +34,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 process_named_running from vyos.util import run from vyos.xml import defaults from vyos import ConfigError @@ -81,6 +80,7 @@ def get_config(config=None): # 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: @@ -88,12 +88,16 @@ def get_config(config=None): 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', @@ -109,8 +113,6 @@ def get_config(config=None): get_first_key=True, no_tag_node_value_mangle=True) - import pprint - pprint.pprint(ipsec) return ipsec def get_rsa_local_key(ipsec): @@ -326,6 +328,11 @@ def generate(ipsec): 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']: @@ -385,24 +392,17 @@ def resync_nhrp(ipsec): def apply(ipsec): if not ipsec: - call('sudo /usr/sbin/ipsec stop') + call('sudo ipsec stop') else: - should_start = 'profile' in ipsec or dict_search('site_to_site.peer', ipsec) - - if not process_named_running('charon') and should_start: - args = f'--auto-update {ipsec["auto_update"]}' if 'auto_update' in ipsec else '' - call(f'sudo /usr/sbin/ipsec start {args}') - elif not should_start: - call('sudo /usr/sbin/ipsec stop') - elif ipsec['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') + 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) |