summaryrefslogtreecommitdiff
path: root/src/conf_mode/vpn_ipsec.py
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2024-08-01 13:08:36 +0200
committerGitHub <noreply@github.com>2024-08-01 13:08:36 +0200
commit962ead698e191ff413aaa1585270dfed48100547 (patch)
treed0aefbaeaf2b5d1716dd578060a93eb07618972a /src/conf_mode/vpn_ipsec.py
parentb12cd41000bf64950582dc62538be609741aac54 (diff)
parent50cf1746d3ab5e3666a3e502c67d7d853ae7f932 (diff)
downloadvyos-1x-962ead698e191ff413aaa1585270dfed48100547.tar.gz
vyos-1x-962ead698e191ff413aaa1585270dfed48100547.zip
Merge pull request #3221 from lucasec/t5873
T5873: ipsec remote access VPN: support VTI interfaces.
Diffstat (limited to 'src/conf_mode/vpn_ipsec.py')
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py128
1 files changed, 114 insertions, 14 deletions
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index cf82b767f..e8a0bc414 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -21,12 +21,16 @@ import jmespath
from sys import exit
from time import sleep
+from ipaddress import ip_address
+from netaddr import IPNetwork
+from netaddr import IPRange
from vyos.base import Warning
from vyos.config import Config
from vyos.config import config_dict_merge
from vyos.configdep import set_dependents
from vyos.configdep import call_dependents
+from vyos.configdict import get_interface_dict
from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_interface_exists
from vyos.configverify import dynamic_interface_pattern
@@ -47,6 +51,9 @@ from vyos.utils.network import interface_exists
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_args
from vyos.utils.process import call
+from vyos.utils.vti_updown_db import vti_updown_db_exists
+from vyos.utils.vti_updown_db import open_vti_updown_db_for_create_or_update
+from vyos.utils.vti_updown_db import remove_vti_updown_db
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -104,6 +111,8 @@ def get_config(config=None):
ipsec = config_dict_merge(default_values, ipsec)
ipsec['dhcp_interfaces'] = set()
+ ipsec['enabled_vti_interfaces'] = set()
+ ipsec['persistent_vti_interfaces'] = set()
ipsec['dhcp_no_address'] = {}
ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes
ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface'])
@@ -121,6 +130,28 @@ def get_config(config=None):
ipsec['l2tp_ike_default'] = 'aes256-sha1-modp1024,3des-sha1-modp1024'
ipsec['l2tp_esp_default'] = 'aes256-sha1,3des-sha1'
+ # Collect the interface dicts for any refernced VTI interfaces in
+ # case we need to bring the interface up
+ ipsec['vti_interface_dicts'] = {}
+
+ if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']:
+ for peer, peer_conf in ipsec['site_to_site']['peer'].items():
+ if 'vti' in peer_conf:
+ if 'bind' in peer_conf['vti']:
+ vti_interface = peer_conf['vti']['bind']
+ if vti_interface not in ipsec['vti_interface_dicts']:
+ _, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface)
+ ipsec['vti_interface_dicts'][vti_interface] = vti
+
+ if 'remote_access' in ipsec:
+ if 'connection' in ipsec['remote_access']:
+ for name, ra_conf in ipsec['remote_access']['connection'].items():
+ if 'bind' in ra_conf:
+ vti_interface = ra_conf['bind']
+ if vti_interface not in ipsec['vti_interface_dicts']:
+ _, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface)
+ ipsec['vti_interface_dicts'][vti_interface] = vti
+
return ipsec
def get_dhcp_address(iface):
@@ -249,7 +280,8 @@ def verify(ipsec):
if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'):
raise ConfigError(f"Invalid dhcp-interface on remote-access connection {name}")
- ipsec['dhcp_interfaces'].add(dhcp_interface)
+ if 'disable' not in ra_conf:
+ ipsec['dhcp_interfaces'].add(dhcp_interface)
address = get_dhcp_address(dhcp_interface)
count = 0
@@ -304,6 +336,16 @@ def verify(ipsec):
if dict_search('remote_access.radius.server', ipsec) == None:
raise ConfigError('RADIUS authentication requires at least one server')
+ if 'bind' in ra_conf:
+ vti_interface = ra_conf['bind']
+ if not interface_exists(vti_interface):
+ raise ConfigError(f'VTI interface {vti_interface} for remote-access connection {name} does not exist!')
+
+ if 'disable' not in ra_conf:
+ ipsec['enabled_vti_interfaces'].add(vti_interface)
+ # remote access VPN interfaces are always up regardless of whether clients are connected
+ ipsec['persistent_vti_interfaces'].add(vti_interface)
+
if 'pool' in ra_conf:
if {'dhcp', 'radius'} <= set(ra_conf['pool']):
raise ConfigError(f'Can not use both DHCP and RADIUS for address allocation '\
@@ -330,26 +372,73 @@ def verify(ipsec):
raise ConfigError(f'Requested pool "{pool}" does not exist!')
if 'pool' in ipsec['remote_access']:
+ pool_networks = []
for pool, pool_config in ipsec['remote_access']['pool'].items():
- if 'prefix' not in pool_config:
- raise ConfigError(f'Missing madatory prefix option for pool "{pool}"!')
+ if 'prefix' not in pool_config and 'range' not in pool_config:
+ raise ConfigError(f'Mandatory prefix or range must be specified for pool "{pool}"!')
+
+ if 'prefix' in pool_config and 'range' in pool_config:
+ raise ConfigError(f'Only one of prefix or range can be specified for pool "{pool}"!')
+
+ if 'prefix' in pool_config:
+ range_is_ipv4 = is_ipv4(pool_config['prefix'])
+ range_is_ipv6 = is_ipv6(pool_config['prefix'])
+
+ net = IPNetwork(pool_config['prefix'])
+ start = net.first
+ stop = net.last
+ for network in pool_networks:
+ if start in network or stop in network:
+ raise ConfigError(f'Prefix for pool "{pool}" is already part of another pool\'s range!')
+
+ tmp = IPRange(start, stop)
+ pool_networks.append(tmp)
+
+ if 'range' in pool_config:
+ range_config = pool_config['range']
+ if not {'start', 'stop'} <= set(range_config.keys()):
+ raise ConfigError(f'Range start and stop address must be defined for pool "{pool}"!')
+
+ range_both_ipv4 = is_ipv4(range_config['start']) and is_ipv4(range_config['stop'])
+ range_both_ipv6 = is_ipv6(range_config['start']) and is_ipv6(range_config['stop'])
+
+ if not (range_both_ipv4 or range_both_ipv6):
+ raise ConfigError(f'Range start and stop must be of the same address family for pool "{pool}"!')
+
+ if ip_address(range_config['stop']) < ip_address(range_config['start']):
+ raise ConfigError(f'Range stop address must be greater or equal\n' \
+ 'to the range\'s start address for pool "{pool}"!')
+
+ range_is_ipv4 = is_ipv4(range_config['start'])
+ range_is_ipv6 = is_ipv6(range_config['start'])
+
+ start = range_config['start']
+ stop = range_config['stop']
+ for network in pool_networks:
+ if start in network:
+ raise ConfigError(f'Range "{range}" start address "{start}" already part of another pool\'s range!')
+ if stop in network:
+ raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another pool\'s range!')
+
+ tmp = IPRange(start, stop)
+ pool_networks.append(tmp)
if 'name_server' in pool_config:
if len(pool_config['name_server']) > 2:
raise ConfigError(f'Only two name-servers are supported for remote-access pool "{pool}"!')
for ns in pool_config['name_server']:
- v4_addr_and_ns = is_ipv4(ns) and not is_ipv4(pool_config['prefix'])
- v6_addr_and_ns = is_ipv6(ns) and not is_ipv6(pool_config['prefix'])
+ v4_addr_and_ns = is_ipv4(ns) and not range_is_ipv4
+ v6_addr_and_ns = is_ipv6(ns) and not range_is_ipv6
if v4_addr_and_ns or v6_addr_and_ns:
- raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and name-server adresses!')
+ raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix/range and name-server addresses!')
if 'exclude' in pool_config:
for exclude in pool_config['exclude']:
- v4_addr_and_exclude = is_ipv4(exclude) and not is_ipv4(pool_config['prefix'])
- v6_addr_and_exclude = is_ipv6(exclude) and not is_ipv6(pool_config['prefix'])
+ v4_addr_and_exclude = is_ipv4(exclude) and not range_is_ipv4
+ v6_addr_and_exclude = is_ipv6(exclude) and not range_is_ipv6
if v4_addr_and_exclude or v6_addr_and_exclude:
- raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and exclude prefixes!')
+ raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix/range and exclude prefixes!')
if 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']:
for server, server_config in ipsec['remote_access']['radius']['server'].items():
@@ -420,7 +509,8 @@ def verify(ipsec):
if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'):
raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}")
- ipsec['dhcp_interfaces'].add(dhcp_interface)
+ if 'disable' not in peer_conf:
+ ipsec['dhcp_interfaces'].add(dhcp_interface)
address = get_dhcp_address(dhcp_interface)
count = 0
@@ -438,14 +528,12 @@ def verify(ipsec):
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 dict_search('options.disable_route_autoinstall',
- ipsec) == None:
- Warning('It\'s recommended to use ipsec vti with the next command\n[set vpn ipsec option disable-route-autoinstall]')
-
if 'bind' in peer_conf['vti']:
vti_interface = peer_conf['vti']['bind']
if not interface_exists(vti_interface):
raise ConfigError(f'VTI interface {vti_interface} for site-to-site peer {peer} does not exist!')
+ if 'disable' not in peer_conf:
+ ipsec['enabled_vti_interfaces'].add(vti_interface)
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}")
@@ -623,9 +711,21 @@ def apply(ipsec):
systemd_service = 'strongswan.service'
if not ipsec:
call(f'systemctl stop {systemd_service}')
+
+ if vti_updown_db_exists():
+ remove_vti_updown_db()
+
else:
call(f'systemctl reload-or-restart {systemd_service}')
+ if ipsec['enabled_vti_interfaces']:
+ with open_vti_updown_db_for_create_or_update() as db:
+ db.removeAllOtherInterfaces(ipsec['enabled_vti_interfaces'])
+ db.setPersistentInterfaces(ipsec['persistent_vti_interfaces'])
+ db.commit(lambda interface: ipsec['vti_interface_dicts'][interface])
+ elif vti_updown_db_exists():
+ remove_vti_updown_db()
+
if ipsec.get('nhrp_exists', False):
try:
call_dependents()