diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 128 | ||||
-rwxr-xr-x | src/etc/ipsec.d/vti-up-down | 53 |
2 files changed, 141 insertions, 40 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() diff --git a/src/etc/ipsec.d/vti-up-down b/src/etc/ipsec.d/vti-up-down index 01e9543c9..e1765ae85 100755 --- a/src/etc/ipsec.d/vti-up-down +++ b/src/etc/ipsec.d/vti-up-down @@ -27,40 +27,41 @@ from syslog import LOG_INFO from vyos.configquery import ConfigTreeQuery from vyos.configdict import get_interface_dict -from vyos.ifconfig import VTIIf +from vyos.utils.commit import wait_for_commit_lock from vyos.utils.process import call -from vyos.utils.network import get_interface_config +from vyos.utils.vti_updown_db import open_vti_updown_db_for_update + +def supply_interface_dict(interface): + # Lazy-load the running config on first invocation + try: + conf = supply_interface_dict.cached_config + except AttributeError: + conf = ConfigTreeQuery() + supply_interface_dict.cached_config = conf + + _, vti = get_interface_dict(conf.config, ['interfaces', 'vti'], interface) + return vti if __name__ == '__main__': verb = os.getenv('PLUTO_VERB') connection = os.getenv('PLUTO_CONNECTION') interface = sys.argv[1] + if verb.endswith('-v6'): + protocol = 'v6' + else: + protocol = 'v4' + openlog(ident=f'vti-up-down', logoption=LOG_PID, facility=LOG_INFO) syslog(f'Interface {interface} {verb} {connection}') - if verb in ['up-client', 'up-host']: - call('sudo ip route delete default table 220') - - vti_link = get_interface_config(interface) - - if not vti_link: - syslog(f'Interface {interface} not found') - sys.exit(0) - - vti_link_up = (vti_link['operstate'] != 'DOWN' if 'operstate' in vti_link else False) + wait_for_commit_lock() - if verb in ['up-client', 'up-host']: - if not vti_link_up: - conf = ConfigTreeQuery() - _, vti = get_interface_dict(conf.config, ['interfaces', 'vti'], interface) - if 'disable' not in vti: - tmp = VTIIf(interface) - tmp.update(vti) - call(f'sudo ip link set {interface} up') - else: - call(f'sudo ip link set {interface} down') - syslog(f'Interface {interface} is admin down ...') - elif verb in ['down-client', 'down-host']: - if vti_link_up: - call(f'sudo ip link set {interface} down') + if verb in ['up-client', 'up-client-v6', 'up-host', 'up-host-v6']: + with open_vti_updown_db_for_update() as db: + db.add(interface, connection, protocol) + db.commit(supply_interface_dict) + elif verb in ['down-client', 'down-client-v6', 'down-host', 'down-host-v6']: + with open_vti_updown_db_for_update() as db: + db.remove(interface, connection, protocol) + db.commit(supply_interface_dict) |